• transition
    • 内置组件
    • transition module
    • entering
    • leaving
    • 总结

    transition

    在我们平时的前端项目开发中,经常会遇到如下需求,一个 DOM 节点的插入和删除或者是显示和隐藏,我们不想让它特别生硬,通常会考虑加一些过渡效果。

    Vue.js 除了实现了强大的数据驱动,组件化的能力,也给我们提供了一整套过渡的解决方案。它内置了 <transition> 组件,我们可以利用它配合一些 CSS3 样式很方便地实现过渡动画,也可以利用它配合 JavaScript 的钩子函数实现过渡动画,在下列情形中,可以给任何元素和组件添加 entering/leaving 过渡:

    • 条件渲染 (使用 v-if)
    • 条件展示 (使用 v-show)
    • 动态组件
    • 组件根节点
      那么举一个最简单的实例,如下:
    1. let vm = new Vue({
    2. el: '#app',
    3. template: '<div id="demo">' +
    4. '<button v-on:click="show = !show">' +
    5. 'Toggle' +
    6. '</button>' +
    7. '<transition :appear="true" name="fade">' +
    8. '<p v-if="show">hello</p>' +
    9. '</transition>' +
    10. '</div>',
    11. data() {
    12. return {
    13. show: true
    14. }
    15. }
    16. })
    1. .fade-enter-active, .fade-leave-active {
    2. transition: opacity .5s;
    3. }
    4. .fade-enter, .fade-leave-to {
    5. opacity: 0;
    6. }

    当我们点击按钮切换显示状态的时候,被 <transition> 包裹的内容会有过渡动画。那么接下来我们从源码的角度来分析它的实现原理。

    内置组件

    <transition> 组件和 <keep-alive> 组件一样,都是 Vue 的内置组件,而 <transition> 的定义在 src/platforms/web/runtime/component/transtion.js 中,之所以在这里定义,是因为 <transition> 组件是 web 平台独有的,先来看一下它的实现:

    1. export default {
    2. name: 'transition',
    3. props: transitionProps,
    4. abstract: true,
    5. render (h: Function) {
    6. let children: any = this.$slots.default
    7. if (!children) {
    8. return
    9. }
    10. // filter out text nodes (possible whitespaces)
    11. children = children.filter((c: VNode) => c.tag || isAsyncPlaceholder(c))
    12. /* istanbul ignore if */
    13. if (!children.length) {
    14. return
    15. }
    16. // warn multiple elements
    17. if (process.env.NODE_ENV !== 'production' && children.length > 1) {
    18. warn(
    19. '<transition> can only be used on a single element. Use ' +
    20. '<transition-group> for lists.',
    21. this.$parent
    22. )
    23. }
    24. const mode: string = this.mode
    25. // warn invalid mode
    26. if (process.env.NODE_ENV !== 'production' &&
    27. mode && mode !== 'in-out' && mode !== 'out-in'
    28. ) {
    29. warn(
    30. 'invalid <transition> mode: ' + mode,
    31. this.$parent
    32. )
    33. }
    34. const rawChild: VNode = children[0]
    35. // if this is a component root node and the component's
    36. // parent container node also has transition, skip.
    37. if (hasParentTransition(this.$vnode)) {
    38. return rawChild
    39. }
    40. // apply transition data to child
    41. // use getRealChild() to ignore abstract components e.g. keep-alive
    42. const child: ?VNode = getRealChild(rawChild)
    43. /* istanbul ignore if */
    44. if (!child) {
    45. return rawChild
    46. }
    47. if (this._leaving) {
    48. return placeholder(h, rawChild)
    49. }
    50. // ensure a key that is unique to the vnode type and to this transition
    51. // component instance. This key will be used to remove pending leaving nodes
    52. // during entering.
    53. const id: string = `__transition-${this._uid}-`
    54. child.key = child.key == null
    55. ? child.isComment
    56. ? id + 'comment'
    57. : id + child.tag
    58. : isPrimitive(child.key)
    59. ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
    60. : child.key
    61. const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
    62. const oldRawChild: VNode = this._vnode
    63. const oldChild: VNode = getRealChild(oldRawChild)
    64. // mark v-show
    65. // so that the transition module can hand over the control to the directive
    66. if (child.data.directives && child.data.directives.some(d => d.name === 'show')) {
    67. child.data.show = true
    68. }
    69. if (
    70. oldChild &&
    71. oldChild.data &&
    72. !isSameChild(child, oldChild) &&
    73. !isAsyncPlaceholder(oldChild) &&
    74. // #6687 component root is a comment node
    75. !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)
    76. ) {
    77. // replace old child transition data with fresh one
    78. // important for dynamic transitions!
    79. const oldData: Object = oldChild.data.transition = extend({}, data)
    80. // handle transition mode
    81. if (mode === 'out-in') {
    82. // return placeholder node and queue update when leave finishes
    83. this._leaving = true
    84. mergeVNodeHook(oldData, 'afterLeave', () => {
    85. this._leaving = false
    86. this.$forceUpdate()
    87. })
    88. return placeholder(h, rawChild)
    89. } else if (mode === 'in-out') {
    90. if (isAsyncPlaceholder(child)) {
    91. return oldRawChild
    92. }
    93. let delayedLeave
    94. const performLeave = () => { delayedLeave() }
    95. mergeVNodeHook(data, 'afterEnter', performLeave)
    96. mergeVNodeHook(data, 'enterCancelled', performLeave)
    97. mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
    98. }
    99. }
    100. return rawChild
    101. }
    102. }

    <transition> 组件和 <keep-alive> 组件有几点实现类似,同样是抽象组件,同样直接实现 render 函数,同样利用了默认插槽。<transition> 组件非常灵活,支持的 props 非常多:

    1. export const transitionProps = {
    2. name: String,
    3. appear: Boolean,
    4. css: Boolean,
    5. mode: String,
    6. type: String,
    7. enterClass: String,
    8. leaveClass: String,
    9. enterToClass: String,
    10. leaveToClass: String,
    11. enterActiveClass: String,
    12. leaveActiveClass: String,
    13. appearClass: String,
    14. appearActiveClass: String,
    15. appearToClass: String,
    16. duration: [Number, String, Object]
    17. }

    这些配置我们稍后会分析它们的作用,<transition> 组件另一个重要的就是 render 函数的实现,render 函数主要作用就是渲染生成 vnode,下面来看一下这部分的逻辑。

    • 处理 children
    1. let children: any = this.$slots.default
    2. if (!children) {
    3. return
    4. }
    5. // filter out text nodes (possible whitespaces)
    6. children = children.filter((c: VNode) => c.tag || isAsyncPlaceholder(c))
    7. /* istanbul ignore if */
    8. if (!children.length) {
    9. return
    10. }
    11. // warn multiple elements
    12. if (process.env.NODE_ENV !== 'production' && children.length > 1) {
    13. warn(
    14. '<transition> can only be used on a single element. Use ' +
    15. '<transition-group> for lists.',
    16. this.$parent
    17. )
    18. }

    先从默认插槽中获取 <transition> 包裹的子节点,并且判断了子节点的长度,如果长度为 0,则直接返回,否则判断长度如果大于 1,也会在开发环境报警告,因为 <transition> 组件是只能包裹一个子节点的。

    • 处理 model
    1. const mode: string = this.mode
    2. // warn invalid mode
    3. if (process.env.NODE_ENV !== 'production' &&
    4. mode && mode !== 'in-out' && mode !== 'out-in'
    5. ) {
    6. warn(
    7. 'invalid <transition> mode: ' + mode,
    8. this.$parent
    9. )
    10. }

    过渡组件的对 mode 的支持只有 2 种,in-out 或者是 out-in

    • 获取 rawChild & child
    1. const rawChild: VNode = children[0]
    2. // if this is a component root node and the component's
    3. // parent container node also has transition, skip.
    4. if (hasParentTransition(this.$vnode)) {
    5. return rawChild
    6. }
    7. // apply transition data to child
    8. // use getRealChild() to ignore abstract components e.g. keep-alive
    9. const child: ?VNode = getRealChild(rawChild)
    10. /* istanbul ignore if */
    11. if (!child) {
    12. return rawChild
    13. }

    rawChild 就是第一个子节点 vnode,接着判断当前 <transition> 如果是组件根节点并且外面包裹该组件的容器也是 <transition> 的时候要跳过。来看一下 hasParentTransition 的实现:

    1. function hasParentTransition (vnode: VNode): ?boolean {
    2. while ((vnode = vnode.parent)) {
    3. if (vnode.data.transition) {
    4. return true
    5. }
    6. }
    7. }

    因为传入的是 this.$vnode,也就是 <transition> 组件的 占位 vnode,只有当它同时作为根 vnode,也就是 vm._vnode 的时候,它的 parent 才不会为空,并且判断 parent 也是 <transition> 组件,才返回 true,vnode.data.transition 我们稍后会介绍。

    getRealChild 的目的是获取组件的非抽象子节点,因为 <transition> 很可能会包裹一个 keep-alive,它的实现如下:

    1. // in case the child is also an abstract component, e.g. <keep-alive>
    2. // we want to recursively retrieve the real component to be rendered
    3. function getRealChild (vnode: ?VNode): ?VNode {
    4. const compOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    5. if (compOptions && compOptions.Ctor.options.abstract) {
    6. return getRealChild(getFirstComponentChild(compOptions.children))
    7. } else {
    8. return vnode
    9. }
    10. }

    会递归找到第一个非抽象组件的 vnode 并返回,在我们这个 case 下,rawChild === child

    • 处理 id & data
    1. // ensure a key that is unique to the vnode type and to this transition
    2. // component instance. This key will be used to remove pending leaving nodes
    3. // during entering.
    4. const id: string = `__transition-${this._uid}-`
    5. child.key = child.key == null
    6. ? child.isComment
    7. ? id + 'comment'
    8. : id + child.tag
    9. : isPrimitive(child.key)
    10. ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
    11. : child.key
    12. const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
    13. const oldRawChild: VNode = this._vnode
    14. const oldChild: VNode = getRealChild(oldRawChild)
    15. // mark v-show
    16. // so that the transition module can hand over the control to the directive
    17. if (child.data.directives && child.data.directives.some(d => d.name === 'show')) {
    18. child.data.show = true
    19. }

    先根据 key 等一系列条件获取 id,接着从当前通过 extractTransitionData 组件实例上提取出过渡所需要的数据:

    1. export function extractTransitionData (comp: Component): Object {
    2. const data = {}
    3. const options: ComponentOptions = comp.$options
    4. // props
    5. for (const key in options.propsData) {
    6. data[key] = comp[key]
    7. }
    8. // events.
    9. // extract listeners and pass them directly to the transition methods
    10. const listeners: ?Object = options._parentListeners
    11. for (const key in listeners) {
    12. data[camelize(key)] = listeners[key]
    13. }
    14. return data
    15. }

    首先是遍历 props 赋值到 data 中,接着是遍历所有父组件的事件也把事件回调赋值到 data 中。

    这样 child.data.transition 中就包含了过渡所需的一些数据,这些稍后都会用到,对于 child 如果使用了 v-show 指令,也会把 child.data.show 设置为 true,在我们的例子中,得到的 child.data 如下:

    1. {
    2. transition: {
    3. appear: true,
    4. name: 'fade'
    5. }
    6. }

    至于 oldRawChildoldChild 是与后面的判断逻辑相关,这些我们这里先不介绍。

    transition module

    刚刚我们介绍完 <transition> 组件的实现,它的 render 阶段只获取了一些数据,并且返回了渲染的 vnode,并没有任何和动画相关,而动画相关的逻辑全部在 src/platforms/web/modules/transition.js 中:

    1. function _enter (_: any, vnode: VNodeWithData) {
    2. if (vnode.data.show !== true) {
    3. enter(vnode)
    4. }
    5. }
    6. export default inBrowser ? {
    7. create: _enter,
    8. activate: _enter,
    9. remove (vnode: VNode, rm: Function) {
    10. /* istanbul ignore else */
    11. if (vnode.data.show !== true) {
    12. leave(vnode, rm)
    13. } else {
    14. rm()
    15. }
    16. }
    17. } : {}

    在之前介绍事件实现的章节中我们提到过在 vnode patch 的过程中,会执行很多钩子函数,那么对于过渡的实现,它只接收了 createactivate 2 个钩子函数,我们知道 create 钩子函数只有当节点的创建过程才会执行,而 remove 会在节点销毁的时候执行,这也就印证了 <transition> 必须要满足 v-if 、动态组件、组件根节点条件之一了,对于 v-show 在它的指令的钩子函数中也会执行相关逻辑,这块儿先不介绍。

    过渡动画提供了 2 个时机,一个是 createactivate 的时候提供了 entering 进入动画,一个是 remove 的时候提供了 leaving 离开动画,那么接下来我们就来分别去分析这两个过程。

    entering

    整个 entering 过程的实现是 enter 函数:

    1. export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
    2. const el: any = vnode.elm
    3. // call leave callback now
    4. if (isDef(el._leaveCb)) {
    5. el._leaveCb.cancelled = true
    6. el._leaveCb()
    7. }
    8. const data = resolveTransition(vnode.data.transition)
    9. if (isUndef(data)) {
    10. return
    11. }
    12. /* istanbul ignore if */
    13. if (isDef(el._enterCb) || el.nodeType !== 1) {
    14. return
    15. }
    16. const {
    17. css,
    18. type,
    19. enterClass,
    20. enterToClass,
    21. enterActiveClass,
    22. appearClass,
    23. appearToClass,
    24. appearActiveClass,
    25. beforeEnter,
    26. enter,
    27. afterEnter,
    28. enterCancelled,
    29. beforeAppear,
    30. appear,
    31. afterAppear,
    32. appearCancelled,
    33. duration
    34. } = data
    35. // activeInstance will always be the <transition> component managing this
    36. // transition. One edge case to check is when the <transition> is placed
    37. // as the root node of a child component. In that case we need to check
    38. // <transition>'s parent for appear check.
    39. let context = activeInstance
    40. let transitionNode = activeInstance.$vnode
    41. while (transitionNode && transitionNode.parent) {
    42. transitionNode = transitionNode.parent
    43. context = transitionNode.context
    44. }
    45. const isAppear = !context._isMounted || !vnode.isRootInsert
    46. if (isAppear && !appear && appear !== '') {
    47. return
    48. }
    49. const startClass = isAppear && appearClass
    50. ? appearClass
    51. : enterClass
    52. const activeClass = isAppear && appearActiveClass
    53. ? appearActiveClass
    54. : enterActiveClass
    55. const toClass = isAppear && appearToClass
    56. ? appearToClass
    57. : enterToClass
    58. const beforeEnterHook = isAppear
    59. ? (beforeAppear || beforeEnter)
    60. : beforeEnter
    61. const enterHook = isAppear
    62. ? (typeof appear === 'function' ? appear : enter)
    63. : enter
    64. const afterEnterHook = isAppear
    65. ? (afterAppear || afterEnter)
    66. : afterEnter
    67. const enterCancelledHook = isAppear
    68. ? (appearCancelled || enterCancelled)
    69. : enterCancelled
    70. const explicitEnterDuration: any = toNumber(
    71. isObject(duration)
    72. ? duration.enter
    73. : duration
    74. )
    75. if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
    76. checkDuration(explicitEnterDuration, 'enter', vnode)
    77. }
    78. const expectsCSS = css !== false && !isIE9
    79. const userWantsControl = getHookArgumentsLength(enterHook)
    80. const cb = el._enterCb = once(() => {
    81. if (expectsCSS) {
    82. removeTransitionClass(el, toClass)
    83. removeTransitionClass(el, activeClass)
    84. }
    85. if (cb.cancelled) {
    86. if (expectsCSS) {
    87. removeTransitionClass(el, startClass)
    88. }
    89. enterCancelledHook && enterCancelledHook(el)
    90. } else {
    91. afterEnterHook && afterEnterHook(el)
    92. }
    93. el._enterCb = null
    94. })
    95. if (!vnode.data.show) {
    96. // remove pending leave element on enter by injecting an insert hook
    97. mergeVNodeHook(vnode, 'insert', () => {
    98. const parent = el.parentNode
    99. const pendingNode = parent && parent._pending && parent._pending[vnode.key]
    100. if (pendingNode &&
    101. pendingNode.tag === vnode.tag &&
    102. pendingNode.elm._leaveCb
    103. ) {
    104. pendingNode.elm._leaveCb()
    105. }
    106. enterHook && enterHook(el, cb)
    107. })
    108. }
    109. // start enter transition
    110. beforeEnterHook && beforeEnterHook(el)
    111. if (expectsCSS) {
    112. addTransitionClass(el, startClass)
    113. addTransitionClass(el, activeClass)
    114. nextFrame(() => {
    115. removeTransitionClass(el, startClass)
    116. if (!cb.cancelled) {
    117. addTransitionClass(el, toClass)
    118. if (!userWantsControl) {
    119. if (isValidDuration(explicitEnterDuration)) {
    120. setTimeout(cb, explicitEnterDuration)
    121. } else {
    122. whenTransitionEnds(el, type, cb)
    123. }
    124. }
    125. }
    126. })
    127. }
    128. if (vnode.data.show) {
    129. toggleDisplay && toggleDisplay()
    130. enterHook && enterHook(el, cb)
    131. }
    132. if (!expectsCSS && !userWantsControl) {
    133. cb()
    134. }
    135. }

    enter 的代码很长,我们先分析其中的核心逻辑。

    • 解析过渡数据
    1. const data = resolveTransition(vnode.data.transition)
    2. if (isUndef(data)) {
    3. return
    4. }
    5. const {
    6. css,
    7. type,
    8. enterClass,
    9. enterToClass,
    10. enterActiveClass,
    11. appearClass,
    12. appearToClass,
    13. appearActiveClass,
    14. beforeEnter,
    15. enter,
    16. afterEnter,
    17. enterCancelled,
    18. beforeAppear,
    19. appear,
    20. afterAppear,
    21. appearCancelled,
    22. duration
    23. } = data

    vnode.data.transition 中解析出过渡相关的一些数据,resolveTransition 的定义在 src/platforms/web/transition-util.js 中:

    1. export function resolveTransition (def?: string | Object): ?Object {
    2. if (!def) {
    3. return
    4. }
    5. /* istanbul ignore else */
    6. if (typeof def === 'object') {
    7. const res = {}
    8. if (def.css !== false) {
    9. extend(res, autoCssTransition(def.name || 'v'))
    10. }
    11. extend(res, def)
    12. return res
    13. } else if (typeof def === 'string') {
    14. return autoCssTransition(def)
    15. }
    16. }
    17. const autoCssTransition: (name: string) => Object = cached(name => {
    18. return {
    19. enterClass: `${name}-enter`,
    20. enterToClass: `${name}-enter-to`,
    21. enterActiveClass: `${name}-enter-active`,
    22. leaveClass: `${name}-leave`,
    23. leaveToClass: `${name}-leave-to`,
    24. leaveActiveClass: `${name}-leave-active`
    25. }
    26. })

    resolveTransition 会通过 autoCssTransition 处理 name 属性,生成一个用来描述各个阶段的 Class 名称的对象,扩展到 def 中并返回给 data,这样我们就可以从 data 中获取到过渡相关的所有数据。

    • 处理边界情况
    1. // activeInstance will always be the <transition> component managing this
    2. // transition. One edge case to check is when the <transition> is placed
    3. // as the root node of a child component. In that case we need to check
    4. // <transition>'s parent for appear check.
    5. let context = activeInstance
    6. let transitionNode = activeInstance.$vnode
    7. while (transitionNode && transitionNode.parent) {
    8. transitionNode = transitionNode.parent
    9. context = transitionNode.context
    10. }
    11. const isAppear = !context._isMounted || !vnode.isRootInsert
    12. if (isAppear && !appear && appear !== '') {
    13. return
    14. }

    这是为了处理当 <transition> 作为子组件的根节点,那么我们需要检查它的父组件作为 appear 的检查。isAppear 表示当前上下文实例还没有 mounted,第一次出现的时机。如果是第一次并且 <transition> 组件没有配置 appear 的话,直接返回。

    • 定义过渡类名、钩子函数和其它配置
    1. const startClass = isAppear && appearClass
    2. ? appearClass
    3. : enterClass
    4. const activeClass = isAppear && appearActiveClass
    5. ? appearActiveClass
    6. : enterActiveClass
    7. const toClass = isAppear && appearToClass
    8. ? appearToClass
    9. : enterToClass
    10. const beforeEnterHook = isAppear
    11. ? (beforeAppear || beforeEnter)
    12. : beforeEnter
    13. const enterHook = isAppear
    14. ? (typeof appear === 'function' ? appear : enter)
    15. : enter
    16. const afterEnterHook = isAppear
    17. ? (afterAppear || afterEnter)
    18. : afterEnter
    19. const enterCancelledHook = isAppear
    20. ? (appearCancelled || enterCancelled)
    21. : enterCancelled
    22. const explicitEnterDuration: any = toNumber(
    23. isObject(duration)
    24. ? duration.enter
    25. : duration
    26. )
    27. if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
    28. checkDuration(explicitEnterDuration, 'enter', vnode)
    29. }
    30. const expectsCSS = css !== false && !isIE9
    31. const userWantsControl = getHookArgumentsLength(enterHook)
    32. const cb = el._enterCb = once(() => {
    33. if (expectsCSS) {
    34. removeTransitionClass(el, toClass)
    35. removeTransitionClass(el, activeClass)
    36. }
    37. if (cb.cancelled) {
    38. if (expectsCSS) {
    39. removeTransitionClass(el, startClass)
    40. }
    41. enterCancelledHook && enterCancelledHook(el)
    42. } else {
    43. afterEnterHook && afterEnterHook(el)
    44. }
    45. el._enterCb = null
    46. })

    对于过渡类名方面,startClass 定义进入过渡的开始状态,在元素被插入时生效,在下一个帧移除;activeClass 定义过渡的状态,在元素整个过渡过程中作用,在元素被插入时生效,在 transition/animation 完成之后移除;toClass 定义进入过渡的结束状态,在元素被插入一帧后生效 (与此同时 startClass 被删除),在 <transition>/animation 完成之后移除。

    对于过渡钩子函数方面,beforeEnterHook 是过渡开始前执行的钩子函数,enterHook 是在元素插入后或者是 v-show 显示切换后执行的钩子函数。afterEnterHook 是在过渡动画执行完后的钩子函数。

    explicitEnterDuration 表示 enter 动画执行的时间。

    expectsCSS 表示过渡动画是受 CSS 的影响。

    cb 定义的是过渡完成执行的回调函数。

    • 合并 insert 钩子函数
    1. if (!vnode.data.show) {
    2. // remove pending leave element on enter by injecting an insert hook
    3. mergeVNodeHook(vnode, 'insert', () => {
    4. const parent = el.parentNode
    5. const pendingNode = parent && parent._pending && parent._pending[vnode.key]
    6. if (pendingNode &&
    7. pendingNode.tag === vnode.tag &&
    8. pendingNode.elm._leaveCb
    9. ) {
    10. pendingNode.elm._leaveCb()
    11. }
    12. enterHook && enterHook(el, cb)
    13. })
    14. }

    mergeVNodeHook 的定义在 src/core/vdom/helpers/merge-hook.js 中:

    1. export function mergeVNodeHook (def: Object, hookKey: string, hook: Function) {
    2. if (def instanceof VNode) {
    3. def = def.data.hook || (def.data.hook = {})
    4. }
    5. let invoker
    6. const oldHook = def[hookKey]
    7. function wrappedHook () {
    8. hook.apply(this, arguments)
    9. // important: remove merged hook to ensure it's called only once
    10. // and prevent memory leak
    11. remove(invoker.fns, wrappedHook)
    12. }
    13. if (isUndef(oldHook)) {
    14. // no existing hook
    15. invoker = createFnInvoker([wrappedHook])
    16. } else {
    17. /* istanbul ignore if */
    18. if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
    19. // already a merged invoker
    20. invoker = oldHook
    21. invoker.fns.push(wrappedHook)
    22. } else {
    23. // existing plain hook
    24. invoker = createFnInvoker([oldHook, wrappedHook])
    25. }
    26. }
    27. invoker.merged = true
    28. def[hookKey] = invoker
    29. }

    mergeVNodeHook 的逻辑很简单,就是把 hook 函数合并到 def.data.hook[hookey] 中,生成新的 invokercreateFnInvoker 方法我们在分析事件章节的时候已经介绍过了。

    我们之前知道组件的 vnode 原本定义了 initprepatchinsertdestroy 四个钩子函数,而 mergeVNodeHook 函数就是把一些新的钩子函数合并进来,例如在 <transition> 过程中合并的 insert 钩子函数,就会合并到组件 vnodeinsert 钩子函数中,这样当组件插入后,就会执行我们定义的 enterHook 了。

    • 开始执行过渡动画
    1. // start enter transition
    2. beforeEnterHook && beforeEnterHook(el)
    3. if (expectsCSS) {
    4. addTransitionClass(el, startClass)
    5. addTransitionClass(el, activeClass)
    6. nextFrame(() => {
    7. removeTransitionClass(el, startClass)
    8. if (!cb.cancelled) {
    9. addTransitionClass(el, toClass)
    10. if (!userWantsControl) {
    11. if (isValidDuration(explicitEnterDuration)) {
    12. setTimeout(cb, explicitEnterDuration)
    13. } else {
    14. whenTransitionEnds(el, type, cb)
    15. }
    16. }
    17. }
    18. })
    19. }

    首先执行 beforeEnterHook 钩子函数,把当前元素的 DOM 节点 el 传入,然后判断 expectsCSS,如果为 true 则表明希望用 CSS 来控制动画,那么会执行 addTransitionClass(el, startClass)addTransitionClass(el, activeClass),它的定义在 src/platforms/runtime/transition-util.js 中:

    1. export function addTransitionClass (el: any, cls: string) {
    2. const transitionClasses = el._transitionClasses || (el._transitionClasses = [])
    3. if (transitionClasses.indexOf(cls) < 0) {
    4. transitionClasses.push(cls)
    5. addClass(el, cls)
    6. }
    7. }

    其实非常简单,就是给当前 DOM 元素 el 添加样式 cls,所以这里添加了 startClassactiveClass,在我们的例子中就是给 p 标签添加了 fade-enterfade-enter-active 2 个样式。

    接下来执行了 nextFrame

    1. const raf = inBrowser
    2. ? window.requestAnimationFrame
    3. ? window.requestAnimationFrame.bind(window)
    4. : setTimeout
    5. : fn => fn()
    6. export function nextFrame (fn: Function) {
    7. raf(() => {
    8. raf(fn)
    9. })
    10. }

    它就是一个简单的 requestAnimationFrame 的实现,它的参数 fn 会在下一帧执行,因此下一帧执行了 removeTransitionClass(el, startClass)

    1. export function removeTransitionClass (el: any, cls: string) {
    2. if (el._transitionClasses) {
    3. remove(el._transitionClasses, cls)
    4. }
    5. removeClass(el, cls)
    6. }

    startClass 移除,在我们的等例子中就是移除 fade-enter 样式。然后判断此时过渡没有被取消,则执行 addTransitionClass(el, toClass) 添加 toClass,在我们的例子中就是添加了 fade-enter-to。然后判断 !userWantsControl,也就是用户不通过 enterHook 钩子函数控制动画,这时候如果用户指定了 explicitEnterDuration,则延时这个时间执行 cb,否则通过 whenTransitionEnds(el, type, cb) 决定执行 cb 的时机:

    1. export function whenTransitionEnds (
    2. el: Element,
    3. expectedType: ?string,
    4. cb: Function
    5. ) {
    6. const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
    7. if (!type) return cb()
    8. const event: string = type === <transition> ? transitionEndEvent : animationEndEvent
    9. let ended = 0
    10. const end = () => {
    11. el.removeEventListener(event, onEnd)
    12. cb()
    13. }
    14. const onEnd = e => {
    15. if (e.target === el) {
    16. if (++ended >= propCount) {
    17. end()
    18. }
    19. }
    20. }
    21. setTimeout(() => {
    22. if (ended < propCount) {
    23. end()
    24. }
    25. }, timeout + 1)
    26. el.addEventListener(event, onEnd)
    27. }

    whenTransitionEnds 的逻辑具体不深讲了,本质上就利用了过渡动画的结束事件来决定 cb 函数的执行。

    最后再回到 cb 函数:

    1. const cb = el._enterCb = once(() => {
    2. if (expectsCSS) {
    3. removeTransitionClass(el, toClass)
    4. removeTransitionClass(el, activeClass)
    5. }
    6. if (cb.cancelled) {
    7. if (expectsCSS) {
    8. removeTransitionClass(el, startClass)
    9. }
    10. enterCancelledHook && enterCancelledHook(el)
    11. } else {
    12. afterEnterHook && afterEnterHook(el)
    13. }
    14. el._enterCb = null
    15. })

    其实很简单,执行了 removeTransitionClass(el, toClass)removeTransitionClass(el, activeClass)toClassactiveClass 移除,然后判断如果有没有取消,如果取消则移除 startClass 并执行 enterCancelledHook,否则执行 afterEnterHook(el)

    那么到这里,entering 的过程就介绍完了。

    leaving

    entering 相对的就是 leaving 阶段了,entering 主要发生在组件插入后,而 leaving 主要发生在组件销毁前。

    1. export function leave (vnode: VNodeWithData, rm: Function) {
    2. const el: any = vnode.elm
    3. // call enter callback now
    4. if (isDef(el._enterCb)) {
    5. el._enterCb.cancelled = true
    6. el._enterCb()
    7. }
    8. const data = resolveTransition(vnode.data.transition)
    9. if (isUndef(data) || el.nodeType !== 1) {
    10. return rm()
    11. }
    12. /* istanbul ignore if */
    13. if (isDef(el._leaveCb)) {
    14. return
    15. }
    16. const {
    17. css,
    18. type,
    19. leaveClass,
    20. leaveToClass,
    21. leaveActiveClass,
    22. beforeLeave,
    23. leave,
    24. afterLeave,
    25. leaveCancelled,
    26. delayLeave,
    27. duration
    28. } = data
    29. const expectsCSS = css !== false && !isIE9
    30. const userWantsControl = getHookArgumentsLength(leave)
    31. const explicitLeaveDuration: any = toNumber(
    32. isObject(duration)
    33. ? duration.leave
    34. : duration
    35. )
    36. if (process.env.NODE_ENV !== 'production' && isDef(explicitLeaveDuration)) {
    37. checkDuration(explicitLeaveDuration, 'leave', vnode)
    38. }
    39. const cb = el._leaveCb = once(() => {
    40. if (el.parentNode && el.parentNode._pending) {
    41. el.parentNode._pending[vnode.key] = null
    42. }
    43. if (expectsCSS) {
    44. removeTransitionClass(el, leaveToClass)
    45. removeTransitionClass(el, leaveActiveClass)
    46. }
    47. if (cb.cancelled) {
    48. if (expectsCSS) {
    49. removeTransitionClass(el, leaveClass)
    50. }
    51. leaveCancelled && leaveCancelled(el)
    52. } else {
    53. rm()
    54. afterLeave && afterLeave(el)
    55. }
    56. el._leaveCb = null
    57. })
    58. if (delayLeave) {
    59. delayLeave(performLeave)
    60. } else {
    61. performLeave()
    62. }
    63. function performLeave () {
    64. // the delayed leave may have already been cancelled
    65. if (cb.cancelled) {
    66. return
    67. }
    68. // record leaving element
    69. if (!vnode.data.show) {
    70. (el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key: any)] = vnode
    71. }
    72. beforeLeave && beforeLeave(el)
    73. if (expectsCSS) {
    74. addTransitionClass(el, leaveClass)
    75. addTransitionClass(el, leaveActiveClass)
    76. nextFrame(() => {
    77. removeTransitionClass(el, leaveClass)
    78. if (!cb.cancelled) {
    79. addTransitionClass(el, leaveToClass)
    80. if (!userWantsControl) {
    81. if (isValidDuration(explicitLeaveDuration)) {
    82. setTimeout(cb, explicitLeaveDuration)
    83. } else {
    84. whenTransitionEnds(el, type, cb)
    85. }
    86. }
    87. }
    88. })
    89. }
    90. leave && leave(el, cb)
    91. if (!expectsCSS && !userWantsControl) {
    92. cb()
    93. }
    94. }
    95. }

    纵观 leave 的实现,和 enter 的实现几乎是一个镜像过程,不同的是从 data 中解析出来的是 leave 相关的样式类名和钩子函数。还有一点不同是可以配置 delayLeave,它是一个函数,可以延时执行 leave 的相关过渡动画,在 leave 动画执行完后,它会执行 rm 函数把节点从 DOM 中真正做移除。

    总结

    那么到此为止基本的 <transition> 过渡的实现分析完毕了,总结起来,Vue 的过渡实现分为以下几个步骤:

    • 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。

    • 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。

    • 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。

    所以真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的 <transition> 只是帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机。

    原文: https://ustbhuangyi.github.io/vue-analysis/extend/tansition.html