对于Vue 3.0 Transition高阶组件仰慕已久,一直很想知道是Transition组件是如何工作的,下面一步一步解析它是如何添加和删除我们常用的动画class。
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
- 组件根节点
Transition如何嗅探是否应用了 CSS 过渡或动画
条件渲染时,Vue在patch函数中mountElement时,通过下例方法来嗅探transition。
// parentSuspense 和 Suspense组件相关,暂时不考虑
const monuntElement = function (vnode, //...) {
const { transition } = vnode
// ...
// 判断是否使用transition
const needCallTransitionHooks =
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition && !transition.persisted
// 通过beforEnter钩子函数初始化动画
if (needCallTransitionHooks) {
transition!.beforeEnter(el)
}
// ...
}
transition通过resolveTransitionHooks方法解析出对内的钩子函数,通过setTransitionHooks方法添加到vnode的transition属性
// BaseTransition.ts
const BaseTransitionImpl = {
setup (props, { slots }) {
return (() => {
// ...
// 过渡钩子作为 vnode.transition 附加到 vnode
const enterHooks = resolveTransitionHooks(
innerChild,
rawProps,
state,
instance
)
setTransitionHooks(child, enterHooks)
// ...
})
}
}
function setTransitionHooks (vnode, hooks) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
setTransitionHooks(vnode.component.subTree, hooks)
} else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
vnode.ssFallback!.transition = hooks.clone(vnode.ssFallback!)
} else {
vnode.transition = hooks // 设置hooks
}
}
resolveTransitionHooks() => TransitionHooks 方法提供enter、leave等方法来处理钩子函数
interface TransitionHooks {
mode: BaseTransitionProps['mode']
persisted: boolean
beforeEnter(el: HostElement): void
enter(el: HostElement): void
leave(el: HostElement, remove: () => void): void
clone(vnode: VNode): TransitionHooks<HostElement>
// optional
afterLeave?(): void
delayLeave?(
el: HostElement,
earlyRemove: () => void,
delayedLeave: () => void
): void
delayedLeave?(): void
}
Transition类名如何在适当的时机添加
当调用TransitionHooks时,会触发resolveTransitionProps方法产生的Props对应的钩子函数在适当的时机来添加transition类名,类似我们的v-enter-active,v-from-active…,最后通过 whenTransitionEnds 方法来结束transition动画
export const Transition = (props, { slots }) =>
h(BaseTransition, resolveTransitionProps(props), slots)
@runtime-dom/src/components/Transition
// 内容很多,可自行查看
function resolveTransitionProps() {
// ...
// 如何添加动画Class 用enter举例
const makeEnterHook = (isAppear) => {
return (el, done) => {
const hook = isAppear ? onAppear : onEnter;
const resolve = () => finishEnter(el, isAppear, done);
hook && hook(el, resolve); // 调用使用时传入的钩子函数
nextFrame(() => {
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass);
addTransitionClass(el, isAppear ? appearToClass : enterToClass);
if (!(hook && hook.length > 1)) {
whenTransitionEnds(el, type, enterDuration, resolve);
}
});
};
return {
// ...
onEnter: makeEnterHook(false)
}
};
}
添加/删除transitionClass方法
function addTransitionClass(el, cls) {
cls.split(/\s+/).forEach(c => c && el.classList.add(c));
(el._vtc ||
(el._vtc = new Set())).add(cls);
}
function removeTransitionClass(el, cls) {
cls.split(/\s+/).forEach(c => c && el.classList.remove(c));
const { _vtc } = el;
if (_vtc) {
_vtc.delete(cls);
if (!_vtc.size) {
el._vtc = undefined;
}
}
}
vShow条件展示则是在vShow的指令中触发transition的钩子函数
export const vShow = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el) // before
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el) // enter
}
},
updated(el, { value, oldValue }, { transition }) {
// update
if (transition && value !== oldValue) {
if (value) {
transition.beforeEnter(el)
setDisplay(el, true)
transition.enter(el)
} else {
transition.leave(el, () => {
setDisplay(el, false)
})
}
} else {
setDisplay(el, value)
}
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
卸载钩子函数及TransitionClass
当组件进行到unmount方法时,会调用remove方法来卸载组件的transition动画。vShow则是在update指令中进行。
const remove = vnode => {
const { type, el, anchor, transition } = vnode;
// ...
const performRemove = () => {
hostRemove(el);
if (transition && !transition.persisted && transition.afterLeave) {
transition.afterLeave();
}
};
if (vnode.shapeFlag & 1 /* ELEMENT */ &&
transition &&
!transition.persisted) {
const { leave, delayLeave } = transition; // 添加 leave-transition-class
const performLeave = () => leave(el, performRemove);
if (delayLeave) {
delayLeave(vnode.el, performRemove, performLeave);
}
else {
performLeave();
}
}
else {
performRemove();
}
};
总结
本次查看源码大致的知道了Transition的渲染过程,它自身不会渲染一个 DOM 元素,也不会出现在父组件链中,而是render一个子节点。Transition的代码实现很巧妙,通过childVnode的transition属性值来处理动画的进入/离开,真的很有意思,值得学习和思考。
以上是本次Transition的分享,Transition组件内容很丰富。个人理解有限,希望对大家有帮助。如有错漏,请大家指正。