- Contents
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
模版编译之后生成的 render 函数:
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode, Transition as _Transition, withCtx as _withCtx, createVNode as _createVNode, Fragment as _Fragment } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("button", {
onClick: $event => (_ctx.show = !_ctx.show)
}, "Toggle", 8 /* PROPS */, ["onClick"]),
_createVNode(_Transition, null, {
default: _withCtx(() => [
(_ctx.show)
? (_openBlock(), _createElementBlock("p", { key: 0 }, "hello"))
: _createCommentVNode("v-if", true)
], undefined, true),
_: 1 /* STABLE */
})
], 64 /* STABLE_FRAGMENT */))
}
生成的 render 函数主要创建了Transition 组件 vnode,并且有一个默认插槽。
原生 DOM 的过渡
过渡效果(持续时长、运动曲线、过渡属性)本质上是一个 DOM 元素在两种状态间的切换,浏览器会根据过渡效果自行完成 DOM 元素的过渡。
- 创建 DOM 元素
- 将过渡的初始状态和运动过程定义到元素上(即
enter-from
、enter-active
添加到元素上) - 将元素添加到页面上(挂载)
元素的初始状态会生效,页面渲染的时候会将 DOM 元素以初始状态所定义的样式进行展示。
- 接下来需要切换元素的状态,使得元素开始运动。将
enter-from
从 DOM 元素上移除,并将enter-to
这个类添加到 DOM 元素上即可。 - 当过渡完成之后,将
enter-to
和enter-active
从 DOM 元素上移除
// 创建 class 为 box 的 DOM 元素
const el = document.createElement('div')
el.classList.add('box')
// 在 DOM 元素被添加到页面之前,将初始状态和运动过程定义到元素上
el.classList.add('enter-from') // 初始状态
el.classList.add('enter-active') //运动过程
//将元素添加到页面
document.body.appendChild(el)
// 嵌套调用 requestAnimationFrame
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.classList.remove('enter-from') // 移除 enter-from
el.classList.add('enter-to') // 添加 enter-to
})
// 监听 transitionend 事件完成收尾工作
el.addEventListener('transitionend', () => {
el.classList.remove('enter-to') // 移除 enter-to
el.classList.remove('enter-active') // 移除 enter-active
})
})
Transition 组件的实现原理与原生 DOM 的过渡原理一样。只是 Transition 组件是基于虚拟 DOM 实现的。
Transition 组件是在 BaseTransition 的基础上封装的高阶函数式组件。
组件渲染
Transition 组件是一个抽象组件,组件本身不渲染任何实体节点,只渲染第一个子元素节点。
Transition 组件内部只能嵌套一个子元素节点,如果有多个节点需要用 TransitionGroup 组件
如果 Transition 组件内部嵌套的是 KeepAlive 组件,那么它会继续查找 KeepAlive 组件嵌套的第一个子元素节点,来作为渲染的元素节点。如何 KeepAlive 组件内没有嵌套任何子节点,那么它会渲染空的注释节点。
在渲染的过程中,Transition 组件还会通过 resolveTransitionHooks
去定义组件 transition 相应的一些钩子函数(beforeEnter
、enter
、leave
、clone
)对象,然后再通过 setTransitionHooks
函数去把这个钩子函数对象设置到 vnode.transition
上。
钩子函数执行
beforeEnter
beforeEnter(el) {
let hook = onBeforeEnter
if (!state.isMounted) {
if (appear) {
hook = onBeforeAppear || onBeforeEnter
} else {
return
}
}
// for same element (v-show)
if (el._leaveCb) {
el._leaveCb(true /* cancelled */)
}
// for toggled element with same key (v-if)
const leavingVNode = leavingVNodesCache[key]
if (
leavingVNode &&
isSameVNodeType(vnode, leavingVNode) &&
leavingVNode.el!._leaveCb
) {
// force early removal (not cancelled)
leavingVNode.el!._leaveCb()
}
callHook(hook, [el])
}
beforeEnter 钩子函数主要做的事情就是根据 appear
的值和 DOM 是否挂载,来执行 onBeforeEnter
函数或者是 onBeforeAppear
函数。
appear
、onBeforeEnter
、onBeforeAppear
这些变量都是从 props
中获取的。而传递的 props
经过了 resolveTransitionProps
函数的封装
props 封装函数 resolveTransitionProps
export function resolveTransitionProps(rawProps) {
const baseProps = {}
for (const key in rawProps) {
if (!(key in DOMTransitionPropsValidators)) {
;(baseProps as any)[key] = (rawProps as any)[key]
}
}
if (rawProps.css === false) {
return baseProps
}
const {
name = 'v',
type,
duration,
enterFromClass = `${name}-enter-from`,
enterActiveClass = `${name}-enter-active`,
enterToClass = `${name}-enter-to`,
appearFromClass = enterFromClass,
appearActiveClass = enterActiveClass,
appearToClass = enterToClass,
leaveFromClass = `${name}-leave-from`,
leaveActiveClass = `${name}-leave-active`,
leaveToClass = `${name}-leave-to`,
} = rawProps
// legacy transition class compat
const legacyClassEnabled =
__COMPAT__ &&
compatUtils.isCompatEnabled(DeprecationTypes.TRANSITION_CLASSES, null)
let legacyEnterFromClass: string
let legacyAppearFromClass: string
let legacyLeaveFromClass: string
if (__COMPAT__ && legacyClassEnabled) {
const toLegacyClass = (cls: string) => cls.replace(/-from$/, '')
if (!rawProps.enterFromClass) {
legacyEnterFromClass = toLegacyClass(enterFromClass)
}
if (!rawProps.appearFromClass) {
legacyAppearFromClass = toLegacyClass(appearFromClass)
}
if (!rawProps.leaveFromClass) {
legacyLeaveFromClass = toLegacyClass(leaveFromClass)
}
}
const durations = normalizeDuration(duration)
const enterDuration = durations && durations[0]
const leaveDuration = durations && durations[1]
const {
onBeforeEnter,
onEnter,
onEnterCancelled,
onLeave,
onLeaveCancelled,
onBeforeAppear = onBeforeEnter,
onAppear = onEnter,
onAppearCancelled = onEnterCancelled,
} = baseProps
const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
done && done()
}
const finishLeave = (
el: Element & { _isLeaving?: boolean },
done?: () => void
) => {
el._isLeaving = false
removeTransitionClass(el, leaveFromClass)
removeTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveActiveClass)
done && done()
}
const makeEnterHook = (isAppear: boolean) => {
return (el: Element, done: () => void) => {
const hook = isAppear ? onAppear : onEnter
const resolve = () => finishEnter(el, isAppear, done)
callHook(hook, [el, resolve])
nextFrame(() => {
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(
el,
isAppear ? legacyAppearFromClass : legacyEnterFromClass
)
}
addTransitionClass(el, isAppear ? appearToClass : enterToClass)
if (!hasExplicitCallback(hook)) {
whenTransitionEnds(el, type, enterDuration, resolve)
}
})
}
}
return extend(baseProps, {
onBeforeEnter(el) {
callHook(onBeforeEnter, [el])
addTransitionClass(el, enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyEnterFromClass)
}
addTransitionClass(el, enterActiveClass)
},
onBeforeAppear(el) {
callHook(onBeforeAppear, [el])
addTransitionClass(el, appearFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyAppearFromClass)
}
addTransitionClass(el, appearActiveClass)
},
onEnter: makeEnterHook(false),
onAppear: makeEnterHook(true),
onLeave(el: Element & { _isLeaving?: boolean }, done) {
el._isLeaving = true
const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyLeaveFromClass)
}
// force reflow so *-leave-from classes immediately take effect (#2593)
forceReflow()
addTransitionClass(el, leaveActiveClass)
nextFrame(() => {
if (!el._isLeaving) {
// cancelled
return
}
removeTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(el, legacyLeaveFromClass)
}
addTransitionClass(el, leaveToClass)
if (!hasExplicitCallback(onLeave)) {
whenTransitionEnds(el, type, leaveDuration, resolve)
}
})
callHook(onLeave, [el, resolve])
},
onEnterCancelled(el) {
finishEnter(el, false)
callHook(onEnterCancelled, [el])
},
onAppearCancelled(el) {
finishEnter(el, true)
callHook(onAppearCancelled, [el])
},
onLeaveCancelled(el) {
finishLeave(el)
callHook(onLeaveCancelled, [el])
},
})
}
resolveTransitionProps
函数主要作用是,在我们给 Transition 传递的 Props
基础上做一层封装,然后返回一个新的 Props
对象,由于它包含了所有的 Props
处理。
onBeforeEnter
onBeforeEnter
函数,它的内部执行了基础 props
传入的 onBeforeEnter
钩子函数(编写 Transition 组件时添加的 beforeEnter
钩子函数),并且给 DOM 元素 el
添加了 enterActiveClass
和 enterFromClass
样式。
注意:enterActiveClass
默认值是 v-enter-active
,enterFromClass
默认值是 v-enter-from
,如果给 Transition 组件传入了 name
的 prop
,比如 fade
,那么 enterActiveClass
的值就是 fade-enter-active
,enterFromClass
的值就是 fade-enter-from
。
就是在 DOM 元素对象在创建后,插入到页面前做的事情:执行 **beforeEnter**
钩子函数,以及给元素添加相应的 CSS 样式。
onBeforeAppear 和 onBeforeEnter 的逻辑类似。它是在我们给 Transition 组件传入 appear
的 Prop,且首次挂载的时候执行的。
enter
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) {
callAsyncHook(hook, [el, done])
} else {
done()
}
}
enter
钩子函数主要做的事情就是根据 appear
的值和 DOM 是否挂载,执行 onEnter
函数或者是 onAppear
函数,并且这个函数的第二个参数是一个 done
函数,表示过渡动画完成后执行的回调函数,它是异步执行的。在 done
函数的内部,我们会执行 onAfterEnter
函数或者是 onEnterCancelled
函数
注意:当
onEnter
或者onAppear
函数的参数长度小于等于 1 的时候,done
函数在执行完hook
函数后同步执行。
同理,onEnter、onAppear、onAfterEnter 和 onEnterCancelled 函数也是从 Props 传入的。重点看 onEnter 的实现,它是 makeEnterHook(false)
执行后的返回值
const makeEnterHook = (isAppear: boolean) => {
return (el: Element, done: () => void) => {
const hook = isAppear ? onAppear : onEnter
const resolve = () => finishEnter(el, isAppear, done)
callHook(hook, [el, resolve])
nextFrame(() => {
removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(
el,
isAppear ? legacyAppearFromClass : legacyEnterFromClass
)
}
addTransitionClass(el, isAppear ? appearToClass : enterToClass)
if (!hasExplicitCallback(hook)) {
whenTransitionEnds(el, type, enterDuration, resolve)
}
})
}
}
在函数内部,首先执行基础 props
传入的 onEnter
钩子函数(写 Transition 组件时添加的 enter 钩子函数),然后在下一帧给 DOM 元素 el
移除了 enterFromClass
,同时添加了 enterToClass
样式。
注意 enterFromClass
是我们在 beforeEnter
阶段添加的,会在当前阶段移除,新增的 enterToClass
值默认是 v-enter-to
,如果给 Transition 组件传入了 name
的 prop
,比如 fade
,那么 enterToClass
的值就是 fade-enter-to
。
当我们添加了 enterToClass 后,这个时候浏览器就开始根据我们编写的 CSS 进入过渡动画。
动画何时结束?Transition 组件允许我们传入 enterDuration
这个 prop
,它会指定进入过渡的动画时长,当然如果你不指定,Vue.js 内部会监听动画结束事件,然后在动画结束后,执行 finishEnter
函数。
const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
done && done()
}
finishEnter
其实就是给 DOM 元素移除 enterToClass
以及 enterActiveClass
,同时执行 done
函数,进而执行 onAfterEnter
钩子函数。
leave
当元素被删除的时候,会执行 remove
方法,在真正从 DOM 移除元素前且存在过渡的情况下,会执行 vnode.transition
中的 leave
钩子函数,并且把移动 DOM 的方法作为第二个参数传入。
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) {
callAsyncHook(onLeave, [el, done])
} else {
done()
}
}
leave
钩子函数主要做的事情就是执行 props
传入的 onBeforeLeave
钩子函数和 onLeave
函数,onLeave
函数的第二个参数是一个 done
函数,它表示离开过渡动画结束后执行的回调函数。
done
函数内部主要做的事情就是执行 remove
方法移除 DOM,然后执行 onAfterLeave
钩子函数或者是 onLeaveCancelled
函数
onLeave(el: Element & { _isLeaving?: boolean }, done) {
el._isLeaving = true
const resolve = () => finishLeave(el, done)
addTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
addTransitionClass(el, legacyLeaveFromClass)
}
// force reflow so *-leave-from classes immediately take effect (#2593)
forceReflow()
addTransitionClass(el, leaveActiveClass)
nextFrame(() => {
if (!el._isLeaving) {
// cancelled
return
}
removeTransitionClass(el, leaveFromClass)
if (__COMPAT__ && legacyClassEnabled) {
removeTransitionClass(el, legacyLeaveFromClass)
}
addTransitionClass(el, leaveToClass)
if (!hasExplicitCallback(onLeave)) {
whenTransitionEnds(el, type, leaveDuration, resolve)
}
})
callHook(onLeave, [el, resolve])
}
onLeave
函数首先给 DOM 元素添加 leaveActiveClass
和 leaveFromClass
,并执行基础 props 传入的 onLeave
钩子函数,然后在下一帧移除 leaveFromClass
,并添加 leaveToClass
。
同上,leaveActiveClass
的默认值是 v-leave-active
,leaveFromClass
的默认值是 v-leave-from
,leaveToClass
的默认值是 v-leave-to
。如果给 Transition 组件传入了 name
的 prop
,比如 fade
,那么 leaveActiveClass
的值就是 fade-leave-active
,leaveFromClass
的值就是 fade-leave-from
,leaveToClass
的值就是 fade-leave-to
。
当添加 leaveToClass
时,浏览器就开始根据我们编写的 CSS 执行离开过渡动画,那么动画何时结束呢?
和进入动画类似,Transition 组件允许我们传入 leaveDuration
这个 prop,指定过渡的动画时长,当然如果你不指定,Vue.js 内部会监听动画结束事件,然后在动画结束后,执行 resolve
函数,它是执行 finishLeave
函数的返回值
const finishLeave = (
el: Element & { _isLeaving?: boolean },
done?: () => void
) => {
el._isLeaving = false
removeTransitionClass(el, leaveFromClass)
removeTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveActiveClass)
done && done()
}
finishLeave 函数就是给 DOM 元素移除 leaveFromClass
、leaveToClass
以及 leaveActiveClass
,同时执行 done
函数,进而执行 onAfterLeave
钩子函数
模式 out-in
Vue.js 给 Transition 组件提供了两种模式, in-out
和 out-in
,它们有什么区别呢?
- 在
in-out
模式下,新元素先进行过渡,完成之后当前元素过渡离开。 - 在
out-in
模式下,当前元素先进行过渡,完成之后新元素过渡进入。
主要讨论 out-in 模式,而 in-out 模式很少用到。
const leavingHooks = resolveTransitionHooks(
oldInnerChild,
rawProps,
state,
instance
)
// update old tree's hooks in case of dynamic transition
setTransitionHooks(oldInnerChild, leavingHooks)
// switching between different views
if (mode === 'out-in') {
state.isLeaving = true
// return placeholder node and queue update when leave finishes
leavingHooks.afterLeave = () => {
state.isLeaving = false
instance.update()
}
return emptyPlaceholder(child)
} else if (mode === 'in-out' && innerChild.type !== Comment) {
leavingHooks.delayLeave = (
el: TransitionElement,
earlyRemove,
delayedLeave
) => {
const leavingVNodesCache = getLeavingNodesForType(
state,
oldInnerChild
)
leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
// early removal callback
el._leaveCb = () => {
earlyRemove()
el._leaveCb = undefined
delete enterHooks.delayedLeave
}
enterHooks.delayedLeave = delayedLeave
}
}
}
当模式为 out-in
的时候,会标记 state.isLeaving
为 true
,然后返回一个空的注释节点,同时更新当前元素的钩子函数中的 afterLeave
函数,内部执行 instance.update
重新渲染组件。
这样做就保证了在当前元素执行离开过渡的时候,新元素只渲染成一个注释节点,这样页面上看上去还是只执行当前元素的离开过渡动画。
然后当离开动画执行完毕后,触发了 Transition 组件的重新渲染,这个时候就可以如期渲染新元素并执行进入过渡动画。