18. 实现 Transition
1.Transition 使用
<script type="module">
import {
Transition,
Teleport,
defineAsyncComponent,
createRenderer,
h,
render,
Text,
Fragment,
ref,
reactive,
getCurrentInstance,
onMounted,
provide,
inject,
toRef,
KeepAlive
} from '/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js';
const props = {
onBeforeEnter(el) {
console.log(el, 'beforeEnter');
},
onEnter(el) {
console.log(el, 'enter');
},
onLeave(el) {
console.log(el, 'leave');
}
};
render(
h(Transition, props, {
default: () => {
return h(
'div',
{ style: { width: '100px', height: '100px', background: 'red' } },
'haha'
);
}
}),
app
);
setTimeout(() => {
render(
h(Transition, props, {
default: () => {
return h(
'p',
{ style: { width: '100px', height: '100px', background: 'blue' } },
'world'
);
}
}),
app
);
}, 4000);
setTimeout(() => {
render(
h(Transition, props, {
default: () => {
return h(
'div',
{ style: { width: '100px', height: '100px', background: 'red' } },
'haha'
);
}
}),
app
);
}, 8000);
</script>
<style>
.v-enter-active,
.v-leave-active {
transition: opacity 2s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
2.Transition 核心实现
import { isKeepAlive } from './keepAlive';
import { h } from './h';
function nextFrame(cb) {
requestAnimationFrame(() => {
requestAnimationFrame(cb);
});
}
function resolveTransitionProps(rawProps) {
const {
name = 'v',
enterFromClass = `${name}-enter-from`,
enterActiveClass = `${name}-enter-active`,
enterToClass = `${name}-enter-to`,
leaveFromClass = `${name}-leave-from`,
leaveActiveClass = `${name}-leave-active`,
leaveToClass = `${name}-leave-to`,
onBeforeEnter,
onEnter,
onLeave
} = rawProps;
return {
onBeforeEnter(el) {
onBeforeEnter && onBeforeEnter(el);
el.classList.add(enterFromClass);
el.classList.add(enterActiveClass);
},
onEnter(el, done) {
const resolve = () => {
el.classList.remove(enterActiveClass);
el.classList.remove(enterToClass);
done && done();
};
onEnter && onEnter(el, resolve);
nextFrame(() => {
el.classList.remove(enterFromClass);
el.classList.add(enterToClass);
if (!onEnter || onEnter.length <= 1) {
el.addEventListener('transitionend', resolve);
}
});
},
onLeave(el, done) {
const resolve = () => {
el.classList.remove(leaveActiveClass);
el.classList.remove(leaveToClass);
done && done();
};
el.classList.add(leaveFromClass);
document.body.offsetHeight;
el.classList.add(leaveActiveClass);
nextFrame(() => {
el.classList.remove(leaveFromClass);
el.classList.add(leaveToClass);
if (!onLeave || onLeave.length <= 1) {
el.addEventListener('transitionend', resolve);
}
});
onLeave && onLeave(el, resolve);
}
};
}
export const Transition = (props, { slots }) => {
return h(BaseTransitionImpl, resolveTransitionProps(props), slots);
};
3.Transition 组件
function getKeepAliveChild(vnode) {
return isKeepAlive(vnode)
? vnode.children
? vnode.children.default()
: undefined
: vnode;
}
function resolveTransitionHooks(props) {
let { onBeforeEnter, onEnter, onLeave } = props;
const hooks = {
beforeEnter(el) {
onBeforeEnter && onBeforeEnter(el);
},
enter(el) {
onEnter && onEnter(el);
},
leave(el, remove) {
onLeave && onLeave(el, remove);
}
};
return hooks;
}
export const BaseTransitionImpl = {
name: 'BaseTransition',
props: { onBeforeEnter: Function, onEnter: Function, onLeave: Function },
setup(props, { slots }) {
const instance = getCurrentInstance();
return () => {
const child = slots.default && slots.default();
if (!child) {
return;
}
const innerChild = getKeepAliveChild(child);
const enterHooks = resolveTransitionHooks(props);
innerChild.transition = enterHooks;
const oldChild = instance.subTree;
const oldInnerChild = oldChild && getKeepAliveChild(oldChild);
if (oldInnerChild) {
if (!isSameVNode(innerChild, oldChild)) {
const leavingHooks = resolveTransitionHooks(props);
oldInnerChild.transition = leavingHooks;
}
}
return child;
};
}
};
4.挂载元素
const mountElement = (vnode, container, anchor, parentComponent) => {
const { type, props, children, shapeFlag, transition } = vnode;
const el = (vnode.el = hostCreateElement(type));
if (props) {
for (let key in props) {
hostPatchProp(el, key, null, props[key]);
}
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el);
} else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, children);
}
if (transition) {
transition.beforeEnter(el);
}
hostInsert(el, container, anchor);
if (transition) {
transition.enter(el);
}
};
const remove = (vnode) => {
const { type, el, transition } = vnode;
const performRemove = () => {
hostRemove(el);
};
if (transition) {
transition.leave(el, performRemove);
} else {
performRemove();
}
};
const unmount = (vnode, parentComponent) => {
remove(vnode);
};
import { isKeepAlive } from './keepAlive';
import { h } from './h';
import { isSameVNode } from './vnode';
import { getCurrentInstance } from './component';
function nextFrame(cb) {
requestAnimationFrame(() => {
requestAnimationFrame(cb);
});
}
function resolveTransitionProps(rawProps) {
const {
name = 'v',
enterFromClass = `${name}-enter-from`,
enterActiveClass = `${name}-enter-active`,
enterToClass = `${name}-enter-to`,
leaveFromClass = `${name}-leave-from`,
leaveActiveClass = `${name}-leave-active`,
leaveToClass = `${name}-leave-to`,
onBeforeEnter,
onEnter,
onLeave
} = rawProps;
return {
onBeforeEnter(el) {
onBeforeEnter && onBeforeEnter(el);
el.classList.add(enterFromClass);
el.classList.add(enterActiveClass);
},
onEnter(el) {
const resolve = () => {
el.classList.remove(enterActiveClass);
el.classList.remove(enterToClass);
};
onEnter && onEnter(el, resolve);
nextFrame(() => {
el.classList.remove(enterFromClass);
el.classList.add(enterToClass);
if (!onEnter || onEnter.length <= 1) {
el.addEventListener('transitionend', resolve);
}
});
},
onLeave(el, done) {
const resolve = () => {
el.classList.remove(leaveActiveClass);
el.classList.remove(leaveToClass);
done && done();
};
el.classList.add(leaveFromClass);
document.body.offsetHeight;
el.classList.add(leaveActiveClass);
nextFrame(() => {
el.classList.remove(leaveFromClass);
el.classList.add(leaveToClass);
if (!onLeave || onLeave.length <= 1) {
el.addEventListener('transitionend', resolve);
}
});
onLeave && onLeave(el, resolve);
}
};
}
export const BaseTransitionImpl = {
name: 'BaseTransition',
props: ['onBeforeEnter', 'onEnter', 'onLeave'],
setup(props, { slots }) {
const instance = getCurrentInstance();
return () => {
const child = slots.default && slots.default();
if (!child) {
return;
}
const innerChild = getKeepAliveChild(child);
const enterHooks = resolveTransitionHooks(props);
innerChild.transition = enterHooks;
const oldChild = instance.subTree;
const oldInnerChild = oldChild && getKeepAliveChild(oldChild);
if (oldInnerChild) {
if (!isSameVNode(innerChild, oldChild)) {
const leavingHooks = resolveTransitionHooks(props);
oldInnerChild.transition = leavingHooks;
}
}
return child;
};
}
};
export const Transition = (props, { slots }) => {
return h(BaseTransitionImpl, resolveTransitionProps(props), slots);
};
function resolveTransitionHooks(props) {
let { onBeforeEnter, onEnter, onLeave } = props;
const hooks = {
beforeEnter(el) {
onBeforeEnter && onBeforeEnter(el);
},
enter(el) {
onEnter && onEnter(el);
},
leave(el, remove) {
onLeave && onLeave(el, remove);
}
};
return hooks;
}
function getKeepAliveChild(vnode) {
return isKeepAlive(vnode)
? vnode.children
? vnode.children.default()
: undefined
: vnode;
}