Vue.js 3.0 核心源码解析 - 前百度、滴滴资深技术专家 - 拉勾教育

作为一名前端开发工程师,平时开发页面少不了要写一些过渡动画,通常可以用 CSS 脚本来实现,当然一些时候也会使用 JavaScript 操作 DOM 来实现动画。那么,如果我们使用 Vue.js 技术栈,有没有好的实现动画的方式呢?

答案是肯定的——有,Vue.js 提供了内置的 Transition 组件,它可以让我们轻松实现动画过渡效果。

Transition 组件的用法

如果你还不太熟悉 Transition 组件的使用,我建议你先去看它的官网文档

Transition 组件通常有三类用法:CSS 过渡,CSS 动画和 JavaScript 钩子。我们分别用几个示例来说明,这里我希望你可以敲代码运行感受一下。

首先来看 CSS 过渡:

  1. <template>
  2. <div class="app">
  3. <button @click="show = !show">
  4. Toggle render
  5. </button>
  6. <transition name="fade">
  7. <p v-if="show">hello</p>
  8. </transition>
  9. </div>
  10. </template>
  11. <script>
  12. export default {
  13. data() {
  14. return {
  15. show: true
  16. }
  17. }
  18. }
  19. </script>
  20. <style>
  21. .fade-enter-active,
  22. .fade-leave-active {
  23. transition: opacity 0.5s ease;
  24. }
  25. .fade-enter-from,
  26. .fade-leave-to {
  27. opacity: 0;
  28. }
  29. </style>

CSS 过渡主要定义了一些过渡的 CSS 样式,当我们点击按钮切换文本显隐的时候,就会应用这些 CSS 样式,实现过渡效果。

接着来看 CSS 动画:

  1. <template>
  2. <div class="app">
  3. <button @click="show = !show">Toggle show</button>
  4. <transition name="bounce">
  5. <p v-if="show">Vue is an awesome front-end MVVM framework. We can use it to build multiple apps.</p>
  6. </transition>
  7. </div>
  8. </template>
  9. <script>
  10. export default {
  11. data() {
  12. return {
  13. show: true
  14. }
  15. }
  16. }
  17. </script>
  18. <style>
  19. .bounce-enter-active {
  20. animation: bounce-in 0.5s;
  21. }
  22. .bounce-leave-active {
  23. animation: bounce-in 0.5s reverse;
  24. }
  25. @keyframes bounce-in {
  26. 0% {
  27. transform: scale(0);
  28. }
  29. 50% {
  30. transform: scale(1.5);
  31. }
  32. 100% {
  33. transform: scale(1);
  34. }
  35. }
  36. </style>

和 CSS 过渡类似,CSS 动画主要定义了一些动画的 CSS 样式,当我们去点击按钮切换文本显隐的时候,就会应用这些 CSS 样式,实现动画效果。

最后,是 JavaScript 钩子:

  1. <template>
  2. <div class="app">
  3. <button @click="show = !show">
  4. Toggle render
  5. </button>
  6. <transition
  7. @before-enter="beforeEnter"
  8. @enter="enter"
  9. @before-leave="beforeLeave"
  10. @leave="leave"
  11. css="false"
  12. >
  13. <p v-if="show">hello</p>
  14. </transition>
  15. </div>
  16. </template>
  17. <script>
  18. export default {
  19. data() {
  20. return {
  21. show: true
  22. }
  23. },
  24. methods: {
  25. beforeEnter(el) {
  26. el.style.opacity = 0
  27. el.style.transition = 'opacity 0.5s ease'
  28. },
  29. enter(el) {
  30. this.$el.offsetHeight
  31. el.style.opacity = 1
  32. },
  33. beforeLeave(el) {
  34. el.style.opacity = 1
  35. },
  36. leave(el) {
  37. el.style.transition = 'opacity 0.5s ease'
  38. el.style.opacity = 0
  39. }
  40. }
  41. }
  42. </script>

Transition 组件也允许在一个过渡组件中定义它过渡生命周期的 JavaScript 钩子函数,我们可以在这些钩子函数中编写 JavaScript 操作 DOM 来实现过渡动画效果。

Transition 组件的核心思想

通过前面三个示例,我们不难发现都是在点击按钮时,通过修改 v-if 的条件值来触发过渡动画的。

其实 Transition 组件过渡动画的触发条件有以下四点:

  • 条件渲染 (使用 v-if);
  • 条件展示 (使用 v-show);
  • 动态组件;
  • 组件根节点。

所以你只能在上述四种情况中使用 Transition 组件,在进入 / 离开过渡的时候会有 6 个 class 切换。

  1. v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  3. v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡动画完成之后移除。
  4. v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to:定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡动画完成之后移除。

24 | Transition 组件:过渡动画的实现原理是怎样的?(上) - 图1

其实说白了 Transition 组件的核心思想就是,Transition 包裹的元素插入删除时在适当的时机插入这些 CSS 样式,而这些 CSS 的实现则决定了元素的过渡动画。

大致了解了 Transition 组件的用法和核心思想后,接下来我们就来探究 Transition 组件的实现原理。

Transition 组件的实现原理

为了方便你的理解,我们还是结合示例来分析:

  1. <template>
  2. <div class="app">
  3. <button @click="show = !show">
  4. Toggle render
  5. </button>
  6. <transition name="fade">
  7. <p v-if="show">hello</p>
  8. </transition>
  9. </div>
  10. </template>

先来看模板编译后生成的 render 函数:

  1. import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, Transition as _Transition, withCtx as _withCtx } from "vue"
  2. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  3. return (_openBlock(), _createBlock("template", null, [
  4. _createVNode("div", { class: "app" }, [
  5. _createVNode("button", {
  6. onClick: $event => (_ctx.show = !_ctx.show)
  7. }, " Toggle render ", 8 , ["onClick"]),
  8. _createVNode(_Transition, { name: "fade" }, {
  9. default: _withCtx(() => [
  10. (_ctx.show)
  11. ? (_openBlock(), _createBlock("p", { key: 0 }, "hello"))
  12. : _createCommentVNode("v-if", true)
  13. ]),
  14. _: 1
  15. })
  16. ])
  17. ]))
  18. }

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

我们接着来看 Transition 组件的定义:

  1. const Transition = (props, { slots }) => h(BaseTransition, resolveTransitionProps(props), slots)
  2. const BaseTransition = {
  3. name: `BaseTransition`,
  4. props: {
  5. mode: String,
  6. appear: Boolean,
  7. persisted: Boolean,
  8. onBeforeEnter: TransitionHookValidator,
  9. onEnter: TransitionHookValidator,
  10. onAfterEnter: TransitionHookValidator,
  11. onEnterCancelled: TransitionHookValidator,
  12. onBeforeLeave: TransitionHookValidator,
  13. onLeave: TransitionHookValidator,
  14. onAfterLeave: TransitionHookValidator,
  15. onLeaveCancelled: TransitionHookValidator,
  16. onBeforeAppear: TransitionHookValidator,
  17. onAppear: TransitionHookValidator,
  18. onAfterAppear: TransitionHookValidator,
  19. onAppearCancelled: TransitionHookValidator
  20. },
  21. setup(props, { slots }) {
  22. const instance = getCurrentInstance()
  23. const state = useTransitionState()
  24. let prevTransitionKey
  25. return () => {
  26. const children = slots.default && getTransitionRawChildren(slots.default(), true)
  27. if (!children || !children.length) {
  28. return
  29. }
  30. if ((process.env.NODE_ENV !== 'production') && children.length > 1) {
  31. warn('<transition> can only be used on a single element or component. Use ' +
  32. '<transition-group> for lists.')
  33. }
  34. const rawProps = toRaw(props)
  35. const { mode } = rawProps
  36. if ((process.env.NODE_ENV !== 'production') && mode && !['in-out', 'out-in', 'default'].includes(mode)) {
  37. warn(`invalid <transition> mode: ${mode}`)
  38. }
  39. const child = children[0]
  40. if (state.isLeaving) {
  41. return emptyPlaceholder(child)
  42. }
  43. const innerChild = getKeepAliveChild(child)
  44. if (!innerChild) {
  45. return emptyPlaceholder(child)
  46. }
  47. const enterHooks = resolveTransitionHooks(innerChild, rawProps, state, instance)
  48. setTransitionHooks(innerChild, enterHooks)
  49. const oldChild = instance.subTree
  50. const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
  51. let transitionKeyChanged = false
  52. const { getTransitionKey } = innerChild.type
  53. if (getTransitionKey) {
  54. const key = getTransitionKey()
  55. if (prevTransitionKey === undefined) {
  56. prevTransitionKey = key
  57. }
  58. else if (key !== prevTransitionKey) {
  59. prevTransitionKey = key
  60. transitionKeyChanged = true
  61. }
  62. }
  63. if (oldInnerChild &&
  64. oldInnerChild.type !== Comment &&
  65. (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)) {
  66. const leavingHooks = resolveTransitionHooks(oldInnerChild, rawProps, state, instance)
  67. setTransitionHooks(oldInnerChild, leavingHooks)
  68. if (mode === 'out-in') {
  69. state.isLeaving = true
  70. leavingHooks.afterLeave = () => {
  71. state.isLeaving = false
  72. instance.update()
  73. }
  74. return emptyPlaceholder(child)
  75. }
  76. else if (mode === 'in-out') {
  77. leavingHooks.delayLeave = (el, earlyRemove, delayedLeave) => {
  78. const leavingVNodesCache = getLeavingNodesForType(state, oldInnerChild)
  79. leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
  80. el._leaveCb = () => {
  81. earlyRemove()
  82. el._leaveCb = undefined
  83. delete enterHooks.delayedLeave
  84. }
  85. enterHooks.delayedLeave = delayedLeave
  86. }
  87. }
  88. }
  89. return child
  90. }
  91. }
  92. }

可以看到,Transition 组件是在 BaseTransition 的基础上封装的高阶函数式组件。由于整个 Transition 的实现代码较多,我就挑重点,为你讲清楚整体的实现思路。

我把 Transition 组件的实现分成组件的渲染、钩子函数的执行、模式的应用三个部分去详细说明。

组件的渲染

先来看 Transition 组件是如何渲染的。我们重点看 setup 函数部分的逻辑。

Transition 组件和前面学习的 KeepAlive 组件一样,是一个抽象组件,组件本身不渲染任何实体节点,只渲染第一个子元素节点。

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

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

如果 Transition 组件内部没有嵌套任何子节点,那么它会渲染空的注释节点。

在渲染的过程中,Transition 组件还会通过 resolveTransitionHooks 去定义组件创建和删除阶段的钩子函数对象,然后再通过 setTransitionHooks 函数去把这个钩子函数对象设置到 vnode.transition 上。

渲染过程中,还会判断这是否是一次更新渲染,如果是会对不同的模式执行不同的处理逻辑,我会在后续介绍模式的应用时详细说明。

以上就是 Transition 组件渲染做的事情,你需要记住的是Transition 渲染的是组件嵌套的第一个子元素节点

但是 Transition 是如何在节点的创建和删除过程中设置那些与过渡动画相关的 CSS 的呢?这些都与钩子函数相关,我们先来看 setTransitionHooks 的实现,看看它定义的钩子函数对象是怎样的:

  1. function resolveTransitionHooks(vnode, props, state, instance) {
  2. const { appear, mode, persisted = false, onBeforeEnter, onEnter, onAfterEnter, onEnterCancelled, onBeforeLeave, onLeave, onAfterLeave, onLeaveCancelled, onBeforeAppear, onAppear, onAfterAppear, onAppearCancelled } = props
  3. const key = String(vnode.key)
  4. const leavingVNodesCache = getLeavingNodesForType(state, vnode)
  5. const callHook = (hook, args) => {
  6. hook &&
  7. callWithAsyncErrorHandling(hook, instance, 9 , args)
  8. }
  9. const hooks = {
  10. mode,
  11. persisted,
  12. beforeEnter(el) {
  13. let hook = onBeforeEnter
  14. if (!state.isMounted) {
  15. if (appear) {
  16. hook = onBeforeAppear || onBeforeEnter
  17. }
  18. else {
  19. return
  20. }
  21. }
  22. if (el._leaveCb) {
  23. el._leaveCb(true )
  24. }
  25. const leavingVNode = leavingVNodesCache[key]
  26. if (leavingVNode &&
  27. isSameVNodeType(vnode, leavingVNode) &&
  28. leavingVNode.el._leaveCb) {
  29. leavingVNode.el._leaveCb()
  30. }
  31. callHook(hook, [el])
  32. },
  33. enter(el) {
  34. let hook = onEnter
  35. let afterHook = onAfterEnter
  36. let cancelHook = onEnterCancelled
  37. if (!state.isMounted) {
  38. if (appear) {
  39. hook = onAppear || onEnter
  40. afterHook = onAfterAppear || onAfterEnter
  41. cancelHook = onAppearCancelled || onEnterCancelled
  42. }
  43. else {
  44. return
  45. }
  46. }
  47. let called = false
  48. const done = (el._enterCb = (cancelled) => {
  49. if (called)
  50. return
  51. called = true
  52. if (cancelled) {
  53. callHook(cancelHook, [el])
  54. }
  55. else {
  56. callHook(afterHook, [el])
  57. }
  58. if (hooks.delayedLeave) {
  59. hooks.delayedLeave()
  60. }
  61. el._enterCb = undefined
  62. })
  63. if (hook) {
  64. hook(el, done)
  65. if (hook.length <= 1) {
  66. done()
  67. }
  68. }
  69. else {
  70. done()
  71. }
  72. },
  73. leave(el, remove) {
  74. const key = String(vnode.key)
  75. if (el._enterCb) {
  76. el._enterCb(true )
  77. }
  78. if (state.isUnmounting) {
  79. return remove()
  80. }
  81. callHook(onBeforeLeave, [el])
  82. let called = false
  83. const done = (el._leaveCb = (cancelled) => {
  84. if (called)
  85. return
  86. called = true
  87. remove()
  88. if (cancelled) {
  89. callHook(onLeaveCancelled, [el])
  90. }
  91. else {
  92. callHook(onAfterLeave, [el])
  93. }
  94. el._leaveCb = undefined
  95. if (leavingVNodesCache[key] === vnode) {
  96. delete leavingVNodesCache[key]
  97. }
  98. })
  99. leavingVNodesCache[key] = vnode
  100. if (onLeave) {
  101. onLeave(el, done)
  102. if (onLeave.length <= 1) {
  103. done()
  104. }
  105. }
  106. else {
  107. done()
  108. }
  109. },
  110. clone(vnode) {
  111. return resolveTransitionHooks(vnode, props, state, instance)
  112. }
  113. }
  114. return hooks
  115. }

钩子函数对象定义了 4 个钩子函数,分别是 beforeEnter,enter,leave 和 clone,它们的执行时机是什么,又是怎么处理 我们给 Transition 组件传递的一些 Prop 的?其中,beforeEnter、enter 和 leave 发生在元素的插入和删除阶段,接下来我们就来分析这几个钩子函数的执行过程。

好的,今天我们就先讲到这里,下节课继续分析钩子函数的执行。

本节课的相关代码在源代码中的位置如下:
packages/runtime-core/src/components/BasetTransition.ts
packages/runtime-core/src/renderer.ts
packages/runtime-dom/src/components/Transition.ts