Vue3 源码解读之之内置组件-Transition 组件
Transition 组件的实现原理
Vue 3
内建的 Transition
组件可以为单个元素或单个组件添加过渡效果。它的核心实现原理如下:
- 当
DOM 元素
被挂载时,将过渡动画附加到该DOM 元素
上; - 当
DOM 元素
被卸载时,不会立即卸载DOM 元素
,而是等到附加到该DOM 元素
上的过渡动画执行完成后再卸载它。
Transition 组件的基本结构
// packages/runtime-core/src/components/BaseTransition.ts
const BaseTransitionImpl: ComponentOptions = {
name: `BaseTransition`,
props: {
mode: String,
appear: Boolean,
persisted: Boolean,
// 省略部分代码
},
setup(props: BaseTransitionProps, { slots }: SetupContext) {
// 省略部分代码
}
}
export const BaseTransition = BaseTransitionImpl as any as {
new (): {
$props: BaseTransitionProps<any>
}
}
从上面的代码可以看出,一个组件就是一个选项对象。Transition
组件上有 name、props、setup
等属性。其中 props
中的属性是用户使用 Transition
组件时需要传递给组件的 Props
。setup
函数是组件选项,用于配置组合式 API。
Transition 组件的 setup 函数
// packages/runtime-core/src/components/BaseTransition.ts
setup(props: BaseTransitionProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
const state = useTransitionState()
let prevTransitionKey: any
return () => {
// 通过默认插槽获取需要过渡的元素
const children =
slots.default && getTransitionRawChildren(slots.default(), true)
if (!children || !children.length) {
return
}
// 省略部分代码
// 由于 transition 组件只能包含单个元素或组件,因此需要获取第一个非注释子节点来进行过渡效果的处理。
let child: VNode = children[0]
if (children.length > 1) {
let hasFound = false
// locate first non-comment child
for (const c of children) {
if (c.type !== Comment) {
if (__DEV__ && hasFound) {
// warn more than one non-comment child
warn(
'<transition> can only be used on a single element or component. ' +
'Use <transition-group> for lists.'
)
break
}
child = c
hasFound = true
if (!__DEV__) break
}
}
}
// 省略部分代码
return child
}
}
setup
函数是 Vue 3
新增的组件选项,它通常会返回一个函数或者返回一个对象。在 Transition
组件中,setup
函数返回的是一个函数,该函数将会直接作为组件的render
函数,会渲染添加了过渡效果的元素节点,并且由于 transition
组件只能包含单个元素或组件,因此需要获取第一个非注释子节点来进行过渡效果的处理。。也就是说,Transition
组件本身不会渲染任何额外的内容,它只是通过默认插槽读取过渡元素,并渲染需要过渡的元素。
给过渡元素添加 transition 钩子函数
在 setup
函数中,我们会看到 resolveTransitionHooks
函数和 setTransitionHooks
函数的调用,如下面的代码所示:
// packages/runtime-core/src/components/BaseTransition.ts
setup(props: BaseTransitionProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
const state = useTransitionState()
let prevTransitionKey: any
return () => {
// 省略部分代码
// 获取被 <transition> 组件包裹的 <keep-alive/> 组件。
// 在 keep-alive 组件中,需要对子节点进行缓存处理。由于 keep-alive 组件可以包含多个子节点,因此需要对每个子节点进行比较。
const innerChild = getKeepAliveChild(child)
if (!innerChild) {
return emptyPlaceholder(child)
}
// 初始化 transition 钩子函数
const enterHooks = resolveTransitionHooks(
innerChild,
rawProps,
state,
instance
)
// 给过渡元素添加 transition 钩子函数
setTransitionHooks(innerChild, enterHooks)
// 省略部分代码
// handle mode
if (
oldInnerChild &&
oldInnerChild.type !== Comment &&
(!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)
) {
// 离开时的 动画
const leavingHooks = resolveTransitionHooks(
oldInnerChild,
rawProps,
state,
instance
)
// 由于过渡效果可以是动态的,也就是在运行时根据不同的条件切换不同的过渡效果。
// 在动态过渡中,需要对旧树的钩子函数进行更新,以适应新的过渡效果。
setTransitionHooks(oldInnerChild, leavingHooks)
// 省略部分代码
}
return child
}
}
可以看到,在获取了被 <transition>
组件包裹的 <keep-alive/>
组件时调用了一次 resolveTransitionHooks
函数和 setTransitionHooks
函数。在处理离开时的动画模式
时又调用了一次 resolveTransitionHooks
函数和 setTransitionHooks
函数。那么这两个函数有什么作用呢?接下来,让我们来看看这两个函数的实现。
resolveTransitionHooks 初始化 transition 钩子函数
// 与 transition 相关的钩子函数会被添加到过渡元素的虚拟节点上
// 渲染器在渲染需要过渡的虚拟节点时,会在合适的时机调用这些钩子函数
export function resolveTransitionHooks(
vnode: VNode,
props: BaseTransitionProps<any>,
state: TransitionState,
instance: ComponentInternalInstance
): TransitionHooks {
// Transition 组件的 props
const {
appear,
mode,
persisted = false,
onBeforeEnter,
onEnter,
onAfterEnter,
onEnterCancelled,
onBeforeLeave,
onLeave,
onAfterLeave,
onLeaveCancelled,
onBeforeAppear,
onAppear,
onAfterAppear,
onAppearCancelled
} = props
// 过渡元素虚拟节点的 key
const key = String(vnode.key)
const leavingVNodesCache = getLeavingNodesForType(state, vnode)
const callHook: TransitionHookCaller = (hook, args) => {
hook &&
callWithAsyncErrorHandling(
hook,
instance,
ErrorCodes.TRANSITION_HOOK,
args
)
}
// 添加到过渡元素上的钩子函数
const hooks: TransitionHooks<TransitionElement> = {
mode,
persisted,
// 进入前的过渡动画
beforeEnter(el) {
let hook = onBeforeEnter
if (!state.isMounted) {
if (appear) {
hook = onBeforeAppear || onBeforeEnter
} else {
return
}
}
// 处理同一元素的 v-show 过渡效果
if (el._leaveCb) {
el._leaveCb(true /* cancelled */)
}
// 处理同一元素的 v-if 过渡效果
const leavingVNode = leavingVNodesCache[key]
if (
leavingVNode &&
isSameVNodeType(vnode, leavingVNode) &&
leavingVNode.el!._leaveCb
) {
// 处理旧的 vnode 在新的 vnode 中不存在的情况,调用 _leaveCb 函数,强制删除旧的 vnode
leavingVNode.el!._leaveCb()
}
callHook(hook, [el])
},
// 进入的过渡动画
enter(el) {
let hook = onEnter
let afterHook = onAfterEnter
let cancelHook = onEnterCancelled
if (!state.isMounted) {
if (appear) {
hook = onAppear || onEnter
afterHook = onAfterAppear || onAfterEnter
cancelHook = onAppearCancelled || onEnterCancelled
} else {
return
}
}
let called = false
const done = (el._enterCb = (cancelled?) => {
if (called) return
called = true
if (cancelled) {
callHook(cancelHook, [el])
} else {
callHook(afterHook, [el])
}
if (hooks.delayedLeave) {
hooks.delayedLeave()
}
el._enterCb = undefined
})
if (hook) {
hook(el, done)
if (hook.length <= 1) {
done()
}
} else {
done()
}
},
// 离开的过渡动画
leave(el, remove) {
const key = String(vnode.key)
if (el._enterCb) {
el._enterCb(true /* cancelled */)
}
if (state.isUnmounting) {
return remove()
}
callHook(onBeforeLeave, [el])
let called = false
const done = (el._leaveCb = (cancelled?) => {
if (called) return
called = true
remove()
if (cancelled) {
callHook(onLeaveCancelled, [el])
} else {
callHook(onAfterLeave, [el])
}
el._leaveCb = undefined
if (leavingVNodesCache[key] === vnode) {
delete leavingVNodesCache[key]
}
})
leavingVNodesCache[key] = vnode
if (onLeave) {
onLeave(el, done)
if (onLeave.length <= 1) {
done()
}
} else {
done()
}
},
clone(vnode) {
return resolveTransitionHooks(vnode, props, state, instance)
}
}
return hooks
}
在 resolveTransitionHooks
函数中,首先从 props
对象中解构出 Transition
组件上的属性,比如 apper、mode
属性以及各种过渡事件。
然后定义了一个 hooks
对象,我们重点关注 hooks
对象中的 beforeEnter、enter
和 leave
函数。其中 beforeEnter
函数 和 enter
函数对应着CSS3动画进场过渡效果
(transition
)的 beforeEnter
阶段和 enter
阶段,如下图所示:
在官方文档中,定义了 6 个过渡 class 类名,分别是 v-enter-from、v-enter-active、v-enter-to、v-leave-from、v-leave-active、v-leave-to
。详细介绍可阅读文档。
beforeEnter 阶段
在创建 DOM
元素后并挂载 DOM
元素之前,可以将这个过程视为 beforeEnter
阶段,在这个阶段,这里需要处理同一元素的 v-show
和 v-if
的过渡效果,还有处理旧的 vnode 在新的 vnode 中不存在的情况,调用 _leaveCb
函数,强制删除旧的 vnode。最后会调用 transition.beforeEnter
钩子,从而在元素上添加 v-enter-from
和 v-enter-active
类。
enter 阶段
在挂载 DOM
元素之后,则可以视作 enter
阶段,在这个阶段,会调用 transition.enter
钩子,从而在元素上移除 v-enter-from
类,添加 v-enter-to
类。
leave 阶段
leave
函数对应着动画离场的过渡效果,如下图所示:
在卸载DOM
元素之前的过程,可以将其视作 leave
阶段。在这个阶段,会调用 transition.leave
钩子函数,从而在元素上添加 v-leave-from
、v-leave-active
和 v-leave-to
类。
最后将该 hooks
对象返回,该 hooks
对象将会被谁使用呢?接下来,我们来看 setTransitionHooks
函数。
setTransitionHooks 设置 transition 钩子函数
// 给需要过渡的元素的虚拟节点添加 transition 钩子函数
export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
// vnode 是组件,则递归调用 setTransitionHooks
// 在过渡元素 VNode 对象上添加transition 相应的钩子函数
setTransitionHooks(vnode.component.subTree, hooks)
} else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
// vnode 是 Suspense 组件组件,调用 hooks 对象的 clone 方法
// 设置 transition 相应的钩子函数
vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
vnode.ssFallback!.transition = hooks.clone(vnode.ssFallback!)
} else {
// 在过渡元素 VNode 对象上添加transition 相应的钩子函数
vnode.transition = hooks
}
}
过渡生命周期钩子的执行
渲染器在渲染需要过渡的虚拟节点时,会在合适的时机调用附加到该虚拟节点上的过渡相关的生命周期函数,具体体现在 mountElement
函数以及 unmount
函数中。
在 mountElement 中执行 beforeEnter 和 enter 钩子
在组件挂载的时候,添加进入前和进入中的过渡动画
// packages/runtime-core/src/renderer.ts
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// 省略部分代码
// 判断一个 VNode 是否需要过渡
const needCallTransitionHooks =
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition &&
!transition.persisted
if (needCallTransitionHooks) {
// 1、调用 transition.beforeEnter 钩子,并将 DOM 元素作为参数传递
transition!.beforeEnter(el)
}
// 2、挂载 DOM 元素
hostInsert(el, container, anchor)
if (
(vnodeHook = props && props.onVnodeMounted) ||
needCallTransitionHooks ||
dirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
// 3、调用 transition.enter 钩子,并把 DOM 元素作为参数传递
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
}
从上面的代码可以看到,一共分成三个步骤:
- 在挂载
DOM
元素之前,会调用transition.beforeEnter
钩子 - 挂载
DOM
元素 - 挂载元素之后,会调用
transition.enter
钩子
并且 transition.beforeEnter
和 transition.enter
这两个钩子函数都接收需要过渡的 DOM
元素对象作为第一个参数。
在 unmount 中执行 leave 钩子
组件卸载的时候添加离开(leave
)动画
// packages/runtime-core/src/renderer.ts
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
const {
type,
props,
ref,
children,
dynamicChildren,
shapeFlag,
patchFlag,
dirs,
} = vnode;
// 省略部分代码
if (shapeFlag & ShapeFlags.COMPONENT) {
// 省略部分代码
} else {
// 省略部分代码
if (doRemove) {
remove(vnode);
}
}
// 省略部分代码
};
可以看到,在 unmount
函数中,如果传入的参数 doRemove
为 true ,则调用 remove
方法移除虚拟节点。过渡生命周期leave
钩子的执行,就在 remove
函数中。代码如下:
// packages/runtime-core/src/renderer.ts
const remove: RemoveFn = vnode => {
const { type, el, anchor, transition } = vnode
if (type === Fragment) {
removeFragment(el!, anchor!)
return
}
if (type === Static) {
removeStaticNode(vnode)
return
}
// 将卸载动作封装到 performRemove 函数中
const performRemove = () => {
hostRemove(el!)
if (transition && !transition.persisted && transition.afterLeave) {
// 离开过渡动画
transition.afterLeave()
}
}
if (
vnode.shapeFlag & ShapeFlags.ELEMENT &&
transition &&
!transition.persisted
) {
// 如果需要过渡处理,则调用 transition.leave 钩子,
// 同时将 DOM 元素和 performRemove 函数作为参数传递
const { leave, delayLeave } = transition
const performLeave = () => leave(el!, performRemove)
if (delayLeave) {
// 需要延迟执行 leave
delayLeave(vnode.el!, performRemove, performLeave)
} else {
// 直接执行 leave
performLeave()
}
} else {
// 如果不需要过渡处理,则直接执行卸载操作
performRemove()
}
}
在上面这段代码中,我们将卸载动作封装到 performRemove
函数内。如果 DOM
元素需要过渡处理,那么就需要等待过渡结束后(delayLeave
)再执行 performRemove
函数完成卸载,否则直接调用该函数完成卸载即可。
总结
本文介绍了 Transition
组件的原理与实现。它的核心原理可以总结为:在 DOM 元素挂载时,将过渡动画附加到 DOM 元素上,而在卸载 DOM 元素之前,等到附加到 DOM 元素上的过渡动画执行完成后再卸载 DOM 元素。
在实现过渡动画的过程中,可以将其分为 beforeEnter、enter、leave
等阶段,在不同的阶段执行不同的操作。在 beforeEnter
阶段,会调用 transition.beforeEnter
钩子,在 enter
阶段会调用 transition.enter
钩子,在 leave
阶段则会调用 transition.leave
钩子。
渲染器在执行 DOM 元素的挂载和卸载操作时,会优先检查 vnode
节点是否需要进行过渡,如果需要,则会在合适的时机执行 vnode.transition
对象中定义的过渡动画相关的钩子函数。
转载自:https://juejin.cn/post/7284185214392254525