• Contents
  1. <button @click="show = !show">Toggle</button>
  2. <Transition>
  3. <p v-if="show">hello</p>
  4. </Transition>

模版编译之后生成的 render 函数:

  1. 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"
  2. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  3. return (_openBlock(), _createElementBlock(_Fragment, null, [
  4. _createElementVNode("button", {
  5. onClick: $event => (_ctx.show = !_ctx.show)
  6. }, "Toggle", 8 /* PROPS */, ["onClick"]),
  7. _createVNode(_Transition, null, {
  8. default: _withCtx(() => [
  9. (_ctx.show)
  10. ? (_openBlock(), _createElementBlock("p", { key: 0 }, "hello"))
  11. : _createCommentVNode("v-if", true)
  12. ], undefined, true),
  13. _: 1 /* STABLE */
  14. })
  15. ], 64 /* STABLE_FRAGMENT */))
  16. }

生成的 render 函数主要创建了Transition 组件 vnode,并且有一个默认插槽。


原生 DOM 的过渡

过渡效果(持续时长、运动曲线、过渡属性)本质上是一个 DOM 元素在两种状态间的切换,浏览器会根据过渡效果自行完成 DOM 元素的过渡。

  1. 创建 DOM 元素
  2. 将过渡的初始状态和运动过程定义到元素上(即 enter-fromenter-active 添加到元素上)
  3. 将元素添加到页面上(挂载)

元素的初始状态会生效,页面渲染的时候会将 DOM 元素以初始状态所定义的样式进行展示。

  1. 接下来需要切换元素的状态,使得元素开始运动。将 enter-from 从 DOM 元素上移除,并将 enter-to 这个类添加到 DOM 元素上即可。
  2. 当过渡完成之后,将 enter-toenter-active 从 DOM 元素上移除
  1. // 创建 class 为 box 的 DOM 元素
  2. const el = document.createElement('div')
  3. el.classList.add('box')
  4. // 在 DOM 元素被添加到页面之前,将初始状态和运动过程定义到元素上
  5. el.classList.add('enter-from') // 初始状态
  6. el.classList.add('enter-active') //运动过程
  7. //将元素添加到页面
  8. document.body.appendChild(el)
  9. // 嵌套调用 requestAnimationFrame
  10. requestAnimationFrame(() => {
  11. requestAnimationFrame(() => {
  12. el.classList.remove('enter-from') // 移除 enter-from
  13. el.classList.add('enter-to') // 添加 enter-to
  14. })
  15. // 监听 transitionend 事件完成收尾工作
  16. el.addEventListener('transitionend', () => {
  17. el.classList.remove('enter-to') // 移除 enter-to
  18. el.classList.remove('enter-active') // 移除 enter-active
  19. })
  20. })

Transition 组件的实现原理与原生 DOM 的过渡原理一样。只是 Transition 组件是基于虚拟 DOM 实现的。


Transition 组件是在 BaseTransition 的基础上封装的高阶函数式组件。

组件渲染

Transition 组件是一个抽象组件,组件本身不渲染任何实体节点,只渲染第一个子元素节点。

Transition 组件内部只能嵌套一个子元素节点,如果有多个节点需要用 TransitionGroup 组件

如果 Transition 组件内部嵌套的是 KeepAlive 组件,那么它会继续查找 KeepAlive 组件嵌套的第一个子元素节点,来作为渲染的元素节点。如何 KeepAlive 组件内没有嵌套任何子节点,那么它会渲染空的注释节点。

在渲染的过程中,Transition 组件还会通过 resolveTransitionHooks 去定义组件 transition 相应的一些钩子函数(beforeEnterenterleaveclone)对象,然后再通过 setTransitionHooks 函数去把这个钩子函数对象设置到 vnode.transition 上。

钩子函数执行

beforeEnter

  1. beforeEnter(el) {
  2. let hook = onBeforeEnter
  3. if (!state.isMounted) {
  4. if (appear) {
  5. hook = onBeforeAppear || onBeforeEnter
  6. } else {
  7. return
  8. }
  9. }
  10. // for same element (v-show)
  11. if (el._leaveCb) {
  12. el._leaveCb(true /* cancelled */)
  13. }
  14. // for toggled element with same key (v-if)
  15. const leavingVNode = leavingVNodesCache[key]
  16. if (
  17. leavingVNode &&
  18. isSameVNodeType(vnode, leavingVNode) &&
  19. leavingVNode.el!._leaveCb
  20. ) {
  21. // force early removal (not cancelled)
  22. leavingVNode.el!._leaveCb()
  23. }
  24. callHook(hook, [el])
  25. }

beforeEnter 钩子函数主要做的事情就是根据 appear 的值和 DOM 是否挂载,来执行 onBeforeEnter 函数或者是 onBeforeAppear 函数。

appearonBeforeEnteronBeforeAppear 这些变量都是从 props 中获取的。而传递的 props 经过了 resolveTransitionProps 函数的封装

props 封装函数 resolveTransitionProps

  1. export function resolveTransitionProps(rawProps) {
  2. const baseProps = {}
  3. for (const key in rawProps) {
  4. if (!(key in DOMTransitionPropsValidators)) {
  5. ;(baseProps as any)[key] = (rawProps as any)[key]
  6. }
  7. }
  8. if (rawProps.css === false) {
  9. return baseProps
  10. }
  11. const {
  12. name = 'v',
  13. type,
  14. duration,
  15. enterFromClass = `${name}-enter-from`,
  16. enterActiveClass = `${name}-enter-active`,
  17. enterToClass = `${name}-enter-to`,
  18. appearFromClass = enterFromClass,
  19. appearActiveClass = enterActiveClass,
  20. appearToClass = enterToClass,
  21. leaveFromClass = `${name}-leave-from`,
  22. leaveActiveClass = `${name}-leave-active`,
  23. leaveToClass = `${name}-leave-to`,
  24. } = rawProps
  25. // legacy transition class compat
  26. const legacyClassEnabled =
  27. __COMPAT__ &&
  28. compatUtils.isCompatEnabled(DeprecationTypes.TRANSITION_CLASSES, null)
  29. let legacyEnterFromClass: string
  30. let legacyAppearFromClass: string
  31. let legacyLeaveFromClass: string
  32. if (__COMPAT__ && legacyClassEnabled) {
  33. const toLegacyClass = (cls: string) => cls.replace(/-from$/, '')
  34. if (!rawProps.enterFromClass) {
  35. legacyEnterFromClass = toLegacyClass(enterFromClass)
  36. }
  37. if (!rawProps.appearFromClass) {
  38. legacyAppearFromClass = toLegacyClass(appearFromClass)
  39. }
  40. if (!rawProps.leaveFromClass) {
  41. legacyLeaveFromClass = toLegacyClass(leaveFromClass)
  42. }
  43. }
  44. const durations = normalizeDuration(duration)
  45. const enterDuration = durations && durations[0]
  46. const leaveDuration = durations && durations[1]
  47. const {
  48. onBeforeEnter,
  49. onEnter,
  50. onEnterCancelled,
  51. onLeave,
  52. onLeaveCancelled,
  53. onBeforeAppear = onBeforeEnter,
  54. onAppear = onEnter,
  55. onAppearCancelled = onEnterCancelled,
  56. } = baseProps
  57. const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
  58. removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
  59. removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
  60. done && done()
  61. }
  62. const finishLeave = (
  63. el: Element & { _isLeaving?: boolean },
  64. done?: () => void
  65. ) => {
  66. el._isLeaving = false
  67. removeTransitionClass(el, leaveFromClass)
  68. removeTransitionClass(el, leaveToClass)
  69. removeTransitionClass(el, leaveActiveClass)
  70. done && done()
  71. }
  72. const makeEnterHook = (isAppear: boolean) => {
  73. return (el: Element, done: () => void) => {
  74. const hook = isAppear ? onAppear : onEnter
  75. const resolve = () => finishEnter(el, isAppear, done)
  76. callHook(hook, [el, resolve])
  77. nextFrame(() => {
  78. removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
  79. if (__COMPAT__ && legacyClassEnabled) {
  80. removeTransitionClass(
  81. el,
  82. isAppear ? legacyAppearFromClass : legacyEnterFromClass
  83. )
  84. }
  85. addTransitionClass(el, isAppear ? appearToClass : enterToClass)
  86. if (!hasExplicitCallback(hook)) {
  87. whenTransitionEnds(el, type, enterDuration, resolve)
  88. }
  89. })
  90. }
  91. }
  92. return extend(baseProps, {
  93. onBeforeEnter(el) {
  94. callHook(onBeforeEnter, [el])
  95. addTransitionClass(el, enterFromClass)
  96. if (__COMPAT__ && legacyClassEnabled) {
  97. addTransitionClass(el, legacyEnterFromClass)
  98. }
  99. addTransitionClass(el, enterActiveClass)
  100. },
  101. onBeforeAppear(el) {
  102. callHook(onBeforeAppear, [el])
  103. addTransitionClass(el, appearFromClass)
  104. if (__COMPAT__ && legacyClassEnabled) {
  105. addTransitionClass(el, legacyAppearFromClass)
  106. }
  107. addTransitionClass(el, appearActiveClass)
  108. },
  109. onEnter: makeEnterHook(false),
  110. onAppear: makeEnterHook(true),
  111. onLeave(el: Element & { _isLeaving?: boolean }, done) {
  112. el._isLeaving = true
  113. const resolve = () => finishLeave(el, done)
  114. addTransitionClass(el, leaveFromClass)
  115. if (__COMPAT__ && legacyClassEnabled) {
  116. addTransitionClass(el, legacyLeaveFromClass)
  117. }
  118. // force reflow so *-leave-from classes immediately take effect (#2593)
  119. forceReflow()
  120. addTransitionClass(el, leaveActiveClass)
  121. nextFrame(() => {
  122. if (!el._isLeaving) {
  123. // cancelled
  124. return
  125. }
  126. removeTransitionClass(el, leaveFromClass)
  127. if (__COMPAT__ && legacyClassEnabled) {
  128. removeTransitionClass(el, legacyLeaveFromClass)
  129. }
  130. addTransitionClass(el, leaveToClass)
  131. if (!hasExplicitCallback(onLeave)) {
  132. whenTransitionEnds(el, type, leaveDuration, resolve)
  133. }
  134. })
  135. callHook(onLeave, [el, resolve])
  136. },
  137. onEnterCancelled(el) {
  138. finishEnter(el, false)
  139. callHook(onEnterCancelled, [el])
  140. },
  141. onAppearCancelled(el) {
  142. finishEnter(el, true)
  143. callHook(onAppearCancelled, [el])
  144. },
  145. onLeaveCancelled(el) {
  146. finishLeave(el)
  147. callHook(onLeaveCancelled, [el])
  148. },
  149. })
  150. }

resolveTransitionProps 函数主要作用是,在我们给 Transition 传递的 Props 基础上做一层封装,然后返回一个新的 Props 对象,由于它包含了所有的 Props 处理。

onBeforeEnter

onBeforeEnter 函数,它的内部执行了基础 props 传入的 onBeforeEnter 钩子函数(编写 Transition 组件时添加的 beforeEnter 钩子函数),并且给 DOM 元素 el 添加了 enterActiveClassenterFromClass 样式。

注意:enterActiveClass 默认值是 v-enter-activeenterFromClass 默认值是 v-enter-from,如果给 Transition 组件传入了 nameprop,比如 fade,那么 enterActiveClass 的值就是 fade-enter-activeenterFromClass 的值就是 fade-enter-from

就是在 DOM 元素对象在创建后,插入到页面前做的事情:执行 **beforeEnter** 钩子函数,以及给元素添加相应的 CSS 样式。

onBeforeAppear 和 onBeforeEnter 的逻辑类似。它是在我们给 Transition 组件传入 appear 的 Prop,且首次挂载的时候执行的。

enter

  1. enter(el) {
  2. let hook = onEnter
  3. let afterHook = onAfterEnter
  4. let cancelHook = onEnterCancelled
  5. if (!state.isMounted) {
  6. if (appear) {
  7. hook = onAppear || onEnter
  8. afterHook = onAfterAppear || onAfterEnter
  9. cancelHook = onAppearCancelled || onEnterCancelled
  10. } else {
  11. return
  12. }
  13. }
  14. let called = false
  15. const done = (el._enterCb = (cancelled?) => {
  16. if (called) return
  17. called = true
  18. if (cancelled) {
  19. callHook(cancelHook, [el])
  20. } else {
  21. callHook(afterHook, [el])
  22. }
  23. if (hooks.delayedLeave) {
  24. hooks.delayedLeave()
  25. }
  26. el._enterCb = undefined
  27. })
  28. if (hook) {
  29. callAsyncHook(hook, [el, done])
  30. } else {
  31. done()
  32. }
  33. }

enter 钩子函数主要做的事情就是根据 appear 的值和 DOM 是否挂载,执行 onEnter 函数或者是 onAppear 函数,并且这个函数的第二个参数是一个 done 函数,表示过渡动画完成后执行的回调函数,它是异步执行的。在 done 函数的内部,我们会执行 onAfterEnter 函数或者是 onEnterCancelled 函数

注意:当 onEnter 或者 onAppear 函数的参数长度小于等于 1 的时候,done 函数在执行完 hook 函数后同步执行。

同理,onEnter、onAppear、onAfterEnter 和 onEnterCancelled 函数也是从 Props 传入的。重点看 onEnter 的实现,它是 makeEnterHook(false) 执行后的返回值

  1. const makeEnterHook = (isAppear: boolean) => {
  2. return (el: Element, done: () => void) => {
  3. const hook = isAppear ? onAppear : onEnter
  4. const resolve = () => finishEnter(el, isAppear, done)
  5. callHook(hook, [el, resolve])
  6. nextFrame(() => {
  7. removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
  8. if (__COMPAT__ && legacyClassEnabled) {
  9. removeTransitionClass(
  10. el,
  11. isAppear ? legacyAppearFromClass : legacyEnterFromClass
  12. )
  13. }
  14. addTransitionClass(el, isAppear ? appearToClass : enterToClass)
  15. if (!hasExplicitCallback(hook)) {
  16. whenTransitionEnds(el, type, enterDuration, resolve)
  17. }
  18. })
  19. }
  20. }

在函数内部,首先执行基础 props 传入的 onEnter 钩子函数(写 Transition 组件时添加的 enter 钩子函数),然后在下一帧给 DOM 元素 el 移除了 enterFromClass,同时添加了 enterToClass 样式。

注意 enterFromClass 是我们在 beforeEnter 阶段添加的,会在当前阶段移除,新增的 enterToClass 值默认是 v-enter-to,如果给 Transition 组件传入了 nameprop,比如 fade,那么 enterToClass 的值就是 fade-enter-to

当我们添加了 enterToClass 后,这个时候浏览器就开始根据我们编写的 CSS 进入过渡动画。

动画何时结束?Transition 组件允许我们传入 enterDuration 这个 prop,它会指定进入过渡的动画时长,当然如果你不指定,Vue.js 内部会监听动画结束事件,然后在动画结束后,执行 finishEnter 函数。

  1. const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
  2. removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
  3. removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
  4. done && done()
  5. }

finishEnter 其实就是给 DOM 元素移除 enterToClass 以及 enterActiveClass,同时执行 done 函数,进而执行 onAfterEnter 钩子函数。

leave

当元素被删除的时候,会执行 remove 方法,在真正从 DOM 移除元素前且存在过渡的情况下,会执行 vnode.transition 中的 leave 钩子函数,并且把移动 DOM 的方法作为第二个参数传入。

  1. leave(el, remove) {
  2. const key = String(vnode.key)
  3. if (el._enterCb) {
  4. el._enterCb(true /* cancelled */)
  5. }
  6. if (state.isUnmounting) {
  7. return remove()
  8. }
  9. callHook(onBeforeLeave, [el])
  10. let called = false
  11. const done = (el._leaveCb = (cancelled?) => {
  12. if (called) return
  13. called = true
  14. remove()
  15. if (cancelled) {
  16. callHook(onLeaveCancelled, [el])
  17. } else {
  18. callHook(onAfterLeave, [el])
  19. }
  20. el._leaveCb = undefined
  21. if (leavingVNodesCache[key] === vnode) {
  22. delete leavingVNodesCache[key]
  23. }
  24. })
  25. leavingVNodesCache[key] = vnode
  26. if (onLeave) {
  27. callAsyncHook(onLeave, [el, done])
  28. } else {
  29. done()
  30. }
  31. }

leave 钩子函数主要做的事情就是执行 props 传入的 onBeforeLeave 钩子函数和 onLeave 函数,onLeave 函数的第二个参数是一个 done 函数,它表示离开过渡动画结束后执行的回调函数。

done 函数内部主要做的事情就是执行 remove 方法移除 DOM,然后执行 onAfterLeave 钩子函数或者是 onLeaveCancelled 函数

  1. onLeave(el: Element & { _isLeaving?: boolean }, done) {
  2. el._isLeaving = true
  3. const resolve = () => finishLeave(el, done)
  4. addTransitionClass(el, leaveFromClass)
  5. if (__COMPAT__ && legacyClassEnabled) {
  6. addTransitionClass(el, legacyLeaveFromClass)
  7. }
  8. // force reflow so *-leave-from classes immediately take effect (#2593)
  9. forceReflow()
  10. addTransitionClass(el, leaveActiveClass)
  11. nextFrame(() => {
  12. if (!el._isLeaving) {
  13. // cancelled
  14. return
  15. }
  16. removeTransitionClass(el, leaveFromClass)
  17. if (__COMPAT__ && legacyClassEnabled) {
  18. removeTransitionClass(el, legacyLeaveFromClass)
  19. }
  20. addTransitionClass(el, leaveToClass)
  21. if (!hasExplicitCallback(onLeave)) {
  22. whenTransitionEnds(el, type, leaveDuration, resolve)
  23. }
  24. })
  25. callHook(onLeave, [el, resolve])
  26. }

onLeave 函数首先给 DOM 元素添加 leaveActiveClassleaveFromClass,并执行基础 props 传入的 onLeave 钩子函数,然后在下一帧移除 leaveFromClass,并添加 leaveToClass

同上,leaveActiveClass 的默认值是 v-leave-activeleaveFromClass 的默认值是 v-leave-fromleaveToClass 的默认值是 v-leave-to。如果给 Transition 组件传入了 nameprop,比如 fade,那么 leaveActiveClass 的值就是 fade-leave-activeleaveFromClass 的值就是 fade-leave-fromleaveToClass 的值就是 fade-leave-to

当添加 leaveToClass 时,浏览器就开始根据我们编写的 CSS 执行离开过渡动画,那么动画何时结束呢?

和进入动画类似,Transition 组件允许我们传入 leaveDuration 这个 prop,指定过渡的动画时长,当然如果你不指定,Vue.js 内部会监听动画结束事件,然后在动画结束后,执行 resolve 函数,它是执行 finishLeave 函数的返回值

  1. const finishLeave = (
  2. el: Element & { _isLeaving?: boolean },
  3. done?: () => void
  4. ) => {
  5. el._isLeaving = false
  6. removeTransitionClass(el, leaveFromClass)
  7. removeTransitionClass(el, leaveToClass)
  8. removeTransitionClass(el, leaveActiveClass)
  9. done && done()
  10. }

finishLeave 函数就是给 DOM 元素移除 leaveFromClassleaveToClass 以及 leaveActiveClass,同时执行 done 函数,进而执行 onAfterLeave 钩子函数

模式 out-in

Vue.js 给 Transition 组件提供了两种模式, in-outout-in ,它们有什么区别呢?

  • in-out 模式下,新元素先进行过渡,完成之后当前元素过渡离开。
  • out-in 模式下,当前元素先进行过渡,完成之后新元素过渡进入。

主要讨论 out-in 模式,而 in-out 模式很少用到。

  1. const leavingHooks = resolveTransitionHooks(
  2. oldInnerChild,
  3. rawProps,
  4. state,
  5. instance
  6. )
  7. // update old tree's hooks in case of dynamic transition
  8. setTransitionHooks(oldInnerChild, leavingHooks)
  9. // switching between different views
  10. if (mode === 'out-in') {
  11. state.isLeaving = true
  12. // return placeholder node and queue update when leave finishes
  13. leavingHooks.afterLeave = () => {
  14. state.isLeaving = false
  15. instance.update()
  16. }
  17. return emptyPlaceholder(child)
  18. } else if (mode === 'in-out' && innerChild.type !== Comment) {
  19. leavingHooks.delayLeave = (
  20. el: TransitionElement,
  21. earlyRemove,
  22. delayedLeave
  23. ) => {
  24. const leavingVNodesCache = getLeavingNodesForType(
  25. state,
  26. oldInnerChild
  27. )
  28. leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
  29. // early removal callback
  30. el._leaveCb = () => {
  31. earlyRemove()
  32. el._leaveCb = undefined
  33. delete enterHooks.delayedLeave
  34. }
  35. enterHooks.delayedLeave = delayedLeave
  36. }
  37. }
  38. }

当模式为 out-in 的时候,会标记 state.isLeavingtrue,然后返回一个空的注释节点,同时更新当前元素的钩子函数中的 afterLeave 函数,内部执行 instance.update 重新渲染组件。

这样做就保证了在当前元素执行离开过渡的时候,新元素只渲染成一个注释节点,这样页面上看上去还是只执行当前元素的离开过渡动画。

然后当离开动画执行完毕后,触发了 Transition 组件的重新渲染,这个时候就可以如期渲染新元素并执行进入过渡动画。