• 【Vue.js 3.0源码】Transition 组件:过渡动画的实现原理是怎样的?


    自我介绍:大家好,我是吉帅振的网络日志(其他平台账号名字相同),互联网前端开发工程师,工作5年,去过上海和北京,经历创业公司,加入过阿里本地生活团队,现在郑州北游教育从事编程培训。

    一、前言

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

    二、Transition 组件的用法

    Transition 组件通常有三类用法:CSS 过渡,CSS 动画和 JavaScript 钩子。我们分别用几个示例来说明:

    1.CSS 过渡:

    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

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

    2.CSS 动画:

    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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

    3.JavaScript 钩子:

    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

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

    三、Transition 组件的核心思想

    通过前面三个示例,我们不难发现都是在点击按钮时,通过修改 v-if 的条件值来触发过渡动画的。其实 Transition 组件过渡动画的触发条件有以下四点:条件渲染 (使用 v-if);条件展示 (使用 v-show);动态组件;组件根节点。所以你只能在上述四种情况中使用 Transition 组件,在进入/离开过渡的时候会有 6 个 class 切换。

    v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。

    v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。

    v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡动画完成之后移除。

    v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。

    v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。

    v-leave-to:定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡动画完成之后移除。

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

    四、Transition 组件的实现原理

    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

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

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

    Transition 组件的定义:

    const Transition = (props, { slots }) => h(BaseTransition, resolveTransitionProps(props), slots)
    const BaseTransition = {
      name: `BaseTransition`,
      props: {
        mode: String,
        appear: Boolean,
        persisted: Boolean,
        // enter
        onBeforeEnter: TransitionHookValidator,
        onEnter: TransitionHookValidator,
        onAfterEnter: TransitionHookValidator,
        onEnterCancelled: TransitionHookValidator,
        // leave
        onBeforeLeave: TransitionHookValidator,
        onLeave: TransitionHookValidator,
        onAfterLeave: TransitionHookValidator,
        onLeaveCancelled: TransitionHookValidator,
        // appear
        onBeforeAppear: TransitionHookValidator,
        onAppear: TransitionHookValidator,
        onAfterAppear: TransitionHookValidator,
        onAppearCancelled: TransitionHookValidator
      },
      setup(props, { slots }) {
        const instance = getCurrentInstance()
        const state = useTransitionState()
        let prevTransitionKey
        return () => {
          const children = slots.default && getTransitionRawChildren(slots.default(), true)
          if (!children || !children.length) {
            return
          }
          // Transition 组件只允许一个子元素节点,多个报警告,提示使用 TransitionGroup 组件
          if ((process.env.NODE_ENV !== 'production') && children.length > 1) {
            warn(' can only be used on a single element or component. Use ' +
              ' for lists.')
          }
          // 不需要追踪响应式,所以改成原始值,提升性能
          const rawProps = toRaw(props)
          const { mode } = rawProps
          // 检查 mode 是否合法
          if ((process.env.NODE_ENV !== 'production') && mode && !['in-out', 'out-in', 'default'].includes(mode)) {
            warn(`invalid  mode: ${mode}`)
          }
          // 获取第一个子元素节点
          const child = children[0]
          if (state.isLeaving) {
            return emptyPlaceholder(child)
          }
          // 处理  的情况
          const innerChild = getKeepAliveChild(child)
          if (!innerChild) {
            return emptyPlaceholder(child)
          }
          const enterHooks = resolveTransitionHooks(innerChild, rawProps, state, instance)
            setTransitionHooks(innerChild, enterHooks)
          const oldChild = instance.subTree
          const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
          let transitionKeyChanged = false
          const { getTransitionKey } = innerChild.type
          if (getTransitionKey) {
            const key = getTransitionKey()
            if (prevTransitionKey === undefined) {
              prevTransitionKey = key
            }
            else if (key !== prevTransitionKey) {
              prevTransitionKey = key
              transitionKeyChanged = true
            }
          }
          if (oldInnerChild &&
            oldInnerChild.type !== Comment &&
            (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)) {
            const leavingHooks = resolveTransitionHooks(oldInnerChild, rawProps, state, instance)
            // 更新旧树的钩子函数
            setTransitionHooks(oldInnerChild, leavingHooks)
            // 在两个视图之间切换
            if (mode === 'out-in') {
              state.isLeaving = true
              // 返回空的占位符节点,当离开过渡结束后,重新渲染组件
              leavingHooks.afterLeave = () => {
                state.isLeaving = false
                instance.update()
              }
              return emptyPlaceholder(child)
            }
            else if (mode === 'in-out') {
              leavingHooks.delayLeave = (el, 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
              }
            }
          }
          return child
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

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

    Transition 组件的实现分成组件的渲染、钩子函数的执行、模式的应用三个部分。

    1.组件的渲染

    Transition 组件和前面学习的 KeepAlive 组件一样,是一个抽象组件,组件本身不渲染任何实体节点,只渲染第一个子元素节点。注意,Transition 组件内部只能嵌套一个子元素节点,如果有多个节点需要用 TransitionGroup 组件。如果 Transition 组件内部嵌套的是 KeepAlive 组件,那么它会继续查找 KeepAlive 组件嵌套的第一个子元素节点,来作为渲染的元素节点。如果 Transition 组件内部没有嵌套任何子节点,那么它会渲染空的注释节点。在渲染的过程中,Transition 组件还会通过 resolveTransitionHooks 去定义组件创建和删除阶段的钩子函数对象,然后再通过 setTransitionHooks函数去把这个钩子函数对象设置到 vnode.transition 上。渲染过程中,还会判断这是否是一次更新渲染。

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

    2.钩子函数的执行

    beforeEnter 钩子函数,在 patch 阶段的 mountElement 函数中,在插入元素节点前且存在过渡的条件下会执行 vnode.transition 中的 beforeEnter 函数:

    beforeEnter(el) {
      let hook = onBeforeEnter
      if (!state.isMounted) {
        if (appear) {
          hook = onBeforeAppear || onBeforeEnter
        }
        else {
          return
        }
      }
      if (el._leaveCb) {
        el._leaveCb(true /* cancelled */)
      }
      const leavingVNode = leavingVNodesCache[key]
      if (leavingVNode &&
        isSameVNodeType(vnode, leavingVNode) &&
        leavingVNode.el._leaveCb) {
        leavingVNode.el._leaveCb()
      }
      callHook(hook, [el])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    beforeEnter 钩子函数主要做的事情就是根据 appear 的值和 DOM 是否挂载,来执行 onBeforeEnter 函数或者是 onBeforeAppear 函数,appear、onBeforeEnter、onBeforeAppear 这些变量都是从 props 中获取的:

    const Transition = (props, { slots }) => h(BaseTransition, resolveTransitionProps(props), slots)
    
    • 1

    传递的 props 经过了 resolveTransitionProps 函数的封装:

    function resolveTransitionProps(rawProps) {
      let { name = 'v', type, css = true, 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
      const baseProps = {}
      for (const key in rawProps) {
        if (!(key in DOMTransitionPropsValidators)) {
          baseProps[key] = rawProps[key]
        }
      }
      if (!css) {
        return baseProps
      }
      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, isAppear, done) => {
        removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
        removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
        done && done()
      }
      const finishLeave = (el, done) => {
        removeTransitionClass(el, leaveToClass)
        removeTransitionClass(el, leaveActiveClass)
        done && done()
      }
      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)) {
              if (enterDuration) {
                setTimeout(resolve, enterDuration)
              }
              else {
                whenTransitionEnds(el, type, resolve)
              }
            }
          })
        }
      }
      return extend(baseProps, {
        onBeforeEnter(el) {
          onBeforeEnter && onBeforeEnter(el)
          addTransitionClass(el, enterActiveClass)
          addTransitionClass(el, enterFromClass)
        },
        onBeforeAppear(el) {
          onBeforeAppear && onBeforeAppear(el)
          addTransitionClass(el, appearActiveClass)
          addTransitionClass(el, appearFromClass)
        },
        onEnter: makeEnterHook(false),
        onAppear: makeEnterHook(true),
        onLeave(el, done) {
          const resolve = () => finishLeave(el, done)
          addTransitionClass(el, leaveActiveClass)
          addTransitionClass(el, leaveFromClass)
          nextFrame(() => {
            removeTransitionClass(el, leaveFromClass)
            addTransitionClass(el, leaveToClass)
            if (!(onLeave && onLeave.length > 1)) {
              if (leaveDuration) {
                setTimeout(resolve, leaveDuration)
              }
              else {
                whenTransitionEnds(el, type, resolve)
              }
            }
          })
          onLeave && onLeave(el, resolve)
        },
        onEnterCancelled(el) {
          finishEnter(el, false)
          onEnterCancelled && onEnterCancelled(el)
        },
        onAppearCancelled(el) {
          finishEnter(el, true)
          onAppearCancelled && onAppearCancelled(el)
        },
        onLeaveCancelled(el) {
          finishLeave(el)
          onLeaveCancelled && onLeaveCancelled(el)
        }
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    resolveTransitionProps 函数主要作用是,Transition 传递的 Props 基础上做一层封装,然后返回一个新的 Props 对象,由于它包含了所有的 Props 处理,你不需要一下子了解所有的实现,按需分析即可。 onBeforeEnter 函数,它的内部执行了基础 props 传入的 onBeforeEnter 钩子函数,并且给 DOM 元素 el 添加了 enterActiveClass 和 enterFromClass 样式。其中,props 传入的 onBeforeEnter 函数就是写 Transition 组件时添加的 beforeEnter 钩子函数。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,且首次挂载的时候执行的。执行完 beforeEnter 钩子函数,接着插入元素到页面,然后会执行 vnode.transition 中的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) {
        hook(el, done)
        if (hook.length <= 1) {
          done()
        }
      }
      else {
        done()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    enter 钩子函数主要做的事情就是根据 appear 的值和 DOM 是否挂载,执行 onEnter 函数或者是 onAppear 函数,并且这个函数的第二个参数是一个 done 函数,表示过渡动画完成后执行的回调函数,它是异步执行的。当 onEnter 或者 onAppear 函数的参数长度小于等于 1 的时候,done 函数在执行完 hook 函数后同步执行。在 done 函数的内部,我们会执行 onAfterEnter 函数或者是 onEnterCancelled 函数,onEnter、onAppear、onAfterEnter 和 onEnterCancelled 函数也是从 Props 传入的,我们重点看 onEnter 的实现,它是 makeEnterHook(false) 函数执行后的返回值,如下:

    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)) {
            if (enterDuration) {
              setTimeout(resolve, enterDuration)
            }
            else {
              whenTransitionEnds(el, type, resolve)
            }
          }
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在函数内部,props 传入的 onEnter 钩子函数,然后在下一帧给 DOM 元素 el 移除了 enterFromClass,同时添加了 enterToClass 样式。其中,props 传入的 onEnter 函数就是我们写 Transition 组件时添加的 enter 钩子函数,enterFromClass 是我们在 beforeEnter 阶段添加的,会在当前阶段移除,新增的 enterToClass 值默认是 v-enter-to,如果给 Transition 组件传入了 name 的 prop,比如 fade,那么 enterToClass 的值就是 fade-enter-to。Transition 组件允许我们传入 enterDuration 这个 prop,它会指定进入过渡的动画时长,当然如果你不指定,Vue.js 内部会监听动画结束事件,然后在动画结束后,执行 finishEnter 函数,来看它的实现:

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

    其实就是给 DOM 元素移除 enterToClass 以及 enterActiveClass,同时执行 done 函数,进而执行 onAfterEnter 钩子函数。当元素被删除的时候,会执行 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) {
        onLeave(el, done)
        if (onLeave.length <= 1) {
          done()
        }
      }
      else {
        done()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    leave 钩子函数主要做的事情就是执行 props 传入的 onBeforeLeave 钩子函数和 onLeave 函数,onLeave 函数的第二个参数是一个 done 函数,它表示离开过渡动画结束后执行的回调函数。done 函数内部主要做的事情就是执行 remove 方法移除 DOM,然后执行 onAfterLeave 钩子函数或者是 onLeaveCancelled 函数。

    onLeave 函数的实现:

    onLeave(el, done) {
      const resolve = () => finishLeave(el, done)
      addTransitionClass(el, leaveActiveClass)
      addTransitionClass(el, leaveFromClass)
      nextFrame(() => {
        removeTransitionClass(el, leaveFromClass)
        addTransitionClass(el, leaveToClass)
        if (!(onLeave && onLeave.length > 1)) {
          if (leaveDuration) {
            setTimeout(resolve, leaveDuration)
          }
          else {
            whenTransitionEnds(el, type, resolve)
          }
        }
      })
      onLeave && onLeave(el, resolve)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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。

    Transition 组件允许我们传入 leaveDuration 这个 prop,指定过渡的动画时长,当然如果你不指定,Vue.js 内部会监听动画结束事件,然后在动画结束后,执行 resolve 函数,它是执行 finishLeave 函数的返回值。其实就是给 DOM 元素移除 leaveToClass 以及 leaveActiveClass,同时执行 done 函数,进而执行 onAfterLeave 钩子函数。

    const finishLeave = (el, done) => {
      removeTransitionClass(el, leaveToClass)
      removeTransitionClass(el, leaveActiveClass)
      done && done()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.模式的应用

    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    show 条件为 false 的情况下,显示字符串 hi,你可以运行这个示例,然后会发现这个过渡效果有点生硬,并不理想。给 Transition 组件加一个 out-in 的 mode:

    
      

    hello

    hi

    • 1
    • 2
    • 3
    • 4

    hello 文本先完成离开的过渡后,hi 文本开始进入过渡动画。模式非常适合这种两个元素切换的场景,Vue.js 给 Transition 组件提供了两种模式, in-out 和 out-in ,在 in-out 模式下,新元素先进行过渡,完成之后当前元素过渡离开。在 out-in 模式下,当前元素先进行过渡,完成之后新元素过渡进入。在实际工作中,大部分情况都是在使用 out-in 模式,而 in-out 模式很少用到,所以接下来我们就来分析 out-in 模式的实现原理。

    当我们点击按钮,show 变量由 true 变成 false,会触发当前元素 hello 文本的离开动画,也会同时触发新元素 hi 文本的进入动画。由于动画是同时进行的,而且在离开动画结束之前,当前元素 hello 是没有被移除 DOM 的,所以它还会占位,就把新元素 hi 文本挤到下面去了。当 hello 文本的离开动画执行完毕从 DOM 中删除后,hi 文本才能回到之前的位置。

    out-in 模式的实现:

    const leavingHooks = resolveTransitionHooks(oldInnerChild, rawProps, state, instance)
    setTransitionHooks(oldInnerChild, leavingHooks)
    if (mode === 'out-in') {
      state.isLeaving = true
      leavingHooks.afterLeave = () => {
        state.isLeaving = false
        instance.update()
      }
      return emptyPlaceholder(child)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    当模式为 out-in 的时候,会标记 state.isLeaving 为 true,然后返回一个空的注释节点,同时更新当前元素的钩子函数中的 afterLeave 函数,内部执行 instance.update 重新渲染组件。这样做就保证了在当前元素执行离开过渡的时候,新元素只渲染成一个注释节点,这样页面上看上去还是只执行当前元素的离开过渡动画。

    五、总结

    Transition 组件是如何渲染的,如何执行过渡动画和相应的钩子函数的,以及当两个视图切换时,模式的工作原理是怎样的。

  • 相关阅读:
    基于Netty实现的简单聊天服务组件
    宽瞬时带宽放大器SKY66051-11、SKY66052-11、SKY66041-11、SKY66317-11(RF)适用于通讯网络
    python生成docx文件
    大数据培训课程之RDD传递一个属性
    个人如何进行深度复盘?这6大高效的复盘模型,让你的年终总结如虎添翼!
    Android——认识Android (Android发展简介)(一)
    安全标准汇总
    Android 开源一个USB读写demo,从多个USB设备中选择一个实现外设控制的通信
    软件测试打工人必须掌握的这9项技能.....
    Docker
  • 原文地址:https://blog.csdn.net/qq_42451979/article/details/126096913