Vue2.x 源码 - transition

上一篇:Vue2.x 源码 - keep-alive

Vue 提供了一整套过渡的解决方案,<transition> 组件,我们可以利用它配合一些 CSS 样式很方便的实现过渡动画,也可以利用它配合 JavaScript 的钩子函数实现动画(v-if、v-show、动态组件、组件根节点);


<transition> 组件作为单个元素/组件的过渡效果,只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。

1、源码实现

<transition> 组件和 <keep-alive> 组件⼀样,都是 Vue 的内置组件,⽽ <transition> 的定义在 src/platforms/web/runtime/component/transtion.js 中:

export default {
  name: 'transition',
  props: transitionProps,
  abstract: true,

  render (h: Function) {
  	//利用slot的default 获取子节点
    let children: any = this.$slots.default
    //子节点不存在直接返回
    if (!children) {
      return
    }
    // 过滤文本节点(可能的空白)
    children = children.filter(isNotTextNode)
    //过滤之后不存在子节点直接返回
    if (!children.length) {
      return
    }
    // 警告多个元素
    if (process.env.NODE_ENV !== 'production' && children.length > 1) {
      warn(
        '<transition> can only be used on a single element. Use ' +
        '<transition-group> for lists.',
        this.$parent
      )
    }
    //警告无效的模式
    const mode: string = this.mode
    if (process.env.NODE_ENV !== 'production' &&
      mode && mode !== 'in-out' && mode !== 'out-in'
    ) {
      warn(
        'invalid <transition> mode: ' + mode,
        this.$parent
      )
    }
    const rawChild: VNode = children[0]
    // 如果这是一个组件的根节点,并且该组件的父容器节点也有转换,请跳过。
    if (hasParentTransition(this.$vnode)) {
      return rawChild
    }
    // 使用getRealChild()忽略抽象组件
    const child: ?VNode = getRealChild(rawChild)
    if (!child) {
      return rawChild
    }
    //out-in模式下如果一个keep-alive 返回一个占位符
    if (this._leaving) {
      return placeholder(h, rawChild)
    }
    // 处理id和data
    const id: string = `__transition-${this._uid}-`
    child.key = child.key == null
      ? child.isComment
        ? id + 'comment'
        : id + child.tag
      : isPrimitive(child.key)
        ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
        : child.key
    const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
    const oldRawChild: VNode = this._vnode
    const oldChild: VNode = getRealChild(oldRawChild)
    // 标记 v-show
    // 这样过渡模块就可以把控制权交给指令
    if (child.data.directives && child.data.directives.some(isVShowDirective)) {
      child.data.show = true
    }

    if (
      oldChild &&
      oldChild.data &&
      !isSameChild(child, oldChild) &&
      !isAsyncPlaceholder(oldChild) &&
      !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)
    ) {
      // 用一个对动态转换很重要的新数据替换旧的子转换数据!
      const oldData: Object = oldChild.data.transition = extend({}, data)
      // 处理过渡模式
      if (mode === 'out-in') {
        // 离开结束时返回占位符节点和队列更新
        this._leaving = true
        mergeVNodeHook(oldData, 'afterLeave', () => {
          this._leaving = false
          this.$forceUpdate()
        })
        return placeholder(h, rawChild)
      } else if (mode === 'in-out') {
        if (isAsyncPlaceholder(child)) {
          return oldRawChild
        }
        let delayedLeave
        const performLeave = () => { delayedLeave() }
        mergeVNodeHook(data, 'afterEnter', performLeave)
        mergeVNodeHook(data, 'enterCancelled', performLeave)
        mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
      }
    }
    return rawChild
  }
}

1、从默认插槽中获取<transition> 包裹的⼦节点,并且判断了⼦节点的⻓度,如果⻓度为 0,则直 接返回,否则判断⻓度如果⼤于 1,也会在开发环境报警告,因为 <transition> 组件是只能包裹⼀ 个⼦节点的;
2、过渡组件的对 mode 的⽀持只有 2 种, in-out 或者是 out-in
3、rawChild 就是第⼀个⼦节点 vnode ,接着判断当前 <transition> 如果是组件根节点并且外⾯ 包裹该组件的容器也是 的时候要跳过,这里传⼊的是 this.$vnode ,也就是 <transition> 组件的 占位 vnode ,只有当它同时作为根 vnode ,也就是 vm._vnode 的时候,它的 parent 才不会为空,并且判断 parent 也是 <transition> 组件,才返回 true
4、利用 getRealChild 获取组件的⾮抽象⼦节点,因为 <transition> 很可能会包裹⼀个 keep- alive
5、先根据 key 等⼀系列条件获取 id ,接着从当前通过 extractTransitionData 从父组件实例上提取出过渡所需要的数据(data = props、data.camelize = listeners);
6、对于子组件,如果使用了 v-show 就将 child.data.show 设置为 true
7、最后则是对新旧 child 进行比较,并对部分钩子函数进行 hook merge 等操作;
8、mergeVNodeHook 的逻辑:它会将 hook 函数合并到 def.data.hook[hookKey] 中,生成一个新的 invoker

2、props

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

export const transitionProps = {
  name: String,
  appear: Boolean,
  css: Boolean,
  mode: String,
  type: String,
  enterClass: String,
  leaveClass: String,
  enterToClass: String,
  leaveToClass: String,
  enterActiveClass: String,
  leaveActiveClass: String,
  appearClass: String,
  appearActiveClass: String,
  appearToClass: String,
  duration: [Number, String, Object]
}

name:用于自动生成 CSS 过渡类名;比如 name 为 test ,自动生成:test-enter(过渡开始状态,一帧)、test-enter-to(过渡插入之后和结束之前)、test-enter-active(过渡结束状态,一帧)、test-leave、test-leave-to、test-leave-active等;
appear:是否在初始渲染时使用过渡,默认为 false;
css:是否使用 CSS 过渡类,默认为 true;设置为 false,将只通过组件事件触发注册的 JavaScript 钩子;
mode:控制离开/进入过渡的时间序列,有效的模式有 “out-in” 和 “in-out”;默认同时进行;
type:指定过渡事件类型,侦听过渡何时结束;有效值为 “transition” 和 “animation”;
duration:指定过渡的持续时间,{ enter: number, leave: number }
自定义类名:将name默认生成的类名替换成对应阶段自定义的类名;enterClass、leaveClass、enterToClass、leaveToClass、enterActiveClass、leaveActiveClass、appearClass、appearActiveClass、appearToClass;

3、事件

before-enter:设置过渡进入之前的组件状态;
enter:设置过渡进入完成时的组件状态;
after-enter:设置过渡进入完成之后的组件状态;
enter-cancelled:设置过渡进入取消之后的组件状态;
before-leave:设置过渡离开之前的组件状态;
leave:设置过渡离开完成时的组件状态;
after-leave:设置过渡离开完成之后的组件状态;
leave-cancelled (v-show only):设置过渡离开取消之后的组件状态;
before-appear:设置过渡显示之前的组件状态;
appear:设置过渡显示完成时的组件状态;
after-appear:设置过渡显示完成之后的组件状态;
appear-cancelled:设置过渡显示取消之后的组件状态;

4、enter & leave

<transition> 组件的实现是在 render 阶段只获取了⼀些数据,这些数据包括不同 mode 下绑定的钩子函数以及每一个钩子需要的 data 数据,并且返回了渲染的 vnode ,并没有任何和动画相关的东西,它的动画相关的逻辑全部在 src/platforms/web/modules/transition.js 中:

首先看一下下面代码:

function _enter (_: any, vnode: VNodeWithData) {
  if (vnode.data.show !== true) {
    enter(vnode)
  }
}
export default inBrowser ? {
  create: _enter,
  activate: _enter,
  remove (vnode: VNode, rm: Function) {
    /* istanbul ignore else */
    if (vnode.data.show !== true) {
      leave(vnode, rm)
    } else {
      rm()
    }
  }
} : {}

从上面的代码我们能看出,在动画处理这块,它设定了两个时机,分别是:

1、在 create 和 activate 的时候执行 enter()
2、remove 的时候执行 leave()

enter

export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
  const el: any = vnode.elm
  // 如果有离开的回调函数,直接执行
  if (isDef(el._leaveCb)) {
    el._leaveCb.cancelled = true
    el._leaveCb()
  }
  //解析过渡数据
  const data = resolveTransition(vnode.data.transition)
  if (isUndef(data)) {
    return
  }
  // 如果 el 中存在 _enterCb 或者 el 不是元素节点,则直接返回
  if (isDef(el._enterCb) || el.nodeType !== 1) {
    return
  }
  const {
    css,
    type,
    enterClass,
    enterToClass,
    enterActiveClass,
    appearClass,
    appearToClass,
    appearActiveClass,
    beforeEnter,
    enter,
    afterEnter,
    enterCancelled,
    beforeAppear,
    appear,
    afterAppear,
    appearCancelled,
    duration
  } = data

  // 处理边界
  let context = activeInstance
  let transitionNode = activeInstance.$vnode
  while (transitionNode && transitionNode.parent) {
    context = transitionNode.context
    transitionNode = transitionNode.parent
  }
  const isAppear = !context._isMounted || !vnode.isRootInsert
  if (isAppear && !appear && appear !== '') {
    return
  }
  //定义过渡类名
  const startClass = isAppear && appearClass
    ? appearClass
    : enterClass
  const activeClass = isAppear && appearActiveClass
    ? appearActiveClass
    : enterActiveClass
  const toClass = isAppear && appearToClass
    ? appearToClass
    : enterToClass
  //定义过渡钩⼦函数
  const beforeEnterHook = isAppear
    ? (beforeAppear || beforeEnter)
    : beforeEnter
  const enterHook = isAppear
    ? (typeof appear === 'function' ? appear : enter)
    : enter
  const afterEnterHook = isAppear
    ? (afterAppear || afterEnter)
    : afterEnter
  const enterCancelledHook = isAppear
    ? (appearCancelled || enterCancelled)
    : enterCancelled
  //其它配置
  const explicitEnterDuration: any = toNumber(
    isObject(duration)
      ? duration.enter
      : duration
  )
  // 获取 enter 动画执行时间
  if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
    checkDuration(explicitEnterDuration, 'enter', vnode)
  }
  //过渡是否受到css影响
  const expectsCSS = css !== false && !isIE9
  //用户是否想控制CSS
  const userWantsControl = getHookArgumentsLength(enterHook)
  //定义过渡完成后执行的回调函数
  const cb = el._enterCb = once(() => {
    if (expectsCSS) {
      removeTransitionClass(el, toClass)
      removeTransitionClass(el, activeClass)
    }
    if (cb.cancelled) {
      if (expectsCSS) {
        removeTransitionClass(el, startClass)
      }
      enterCancelledHook && enterCancelledHook(el)
    } else {
      afterEnterHook && afterEnterHook(el)
    }
    el._enterCb = null
  })

  if (!vnode.data.show) {
    // 在进入时注入一个插入钩子来移除挂起的leave元素
    mergeVNodeHook(vnode, 'insert', () => {
      const parent = el.parentNode
      const pendingNode = parent && parent._pending && parent._pending[vnode.key]
      if (pendingNode &&
        pendingNode.tag === vnode.tag &&
        pendingNode.elm._leaveCb
      ) {
        pendingNode.elm._leaveCb()
      }
      enterHook && enterHook(el, cb)
    })
  }
  // 开始执⾏过渡动画
  beforeEnterHook && beforeEnterHook(el)
  if (expectsCSS) {
    addTransitionClass(el, startClass)
    addTransitionClass(el, activeClass)
    nextFrame(() => {
      removeTransitionClass(el, startClass)
      if (!cb.cancelled) {
        addTransitionClass(el, toClass)
        if (!userWantsControl) {
          if (isValidDuration(explicitEnterDuration)) {
            setTimeout(cb, explicitEnterDuration)
          } else {
            whenTransitionEnds(el, type, cb)
          }
        }
      }
    })
  }
  //有show
  if (vnode.data.show) {
    toggleDisplay && toggleDisplay()
    enterHook && enterHook(el, cb)
  }
  //用户不控制,css也不
  if (!expectsCSS && !userWantsControl) {
    cb()
  }
}

1、首先对过渡数据进行处理,resolveTransition 会通过autoCssTransition 来处理 name 属性,生成一个用来描述各个阶段的 class 名称的对象,扩展到 def 种并返回给 data,这样 data 种就可以获取到过渡相关的所有数据了;
2、接下来处理当 <transition> 作为⼦组件的根节点,那么我们需要检查它的⽗组件作为 appear 的检查; isAppear 表⽰当前上下⽂实例还没有 mounted ,第⼀次出现的时机;如果是第⼀次并且 <transition> 组件没有配置 appear 的话,直接返回;
3、对于过渡类名:

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

4、对于过渡钩⼦函数:

beforeEnterHook 是过渡开始前执⾏的钩⼦函数;
enterHook 是在元素 插⼊后或者是 v-show 显⽰切换后执⾏的钩⼦函数;
afterEnterHook 是在过渡动画执⾏完后的钩 ⼦函数;
enterCancelledHook

5、其他:

explicitEnterDuration 动画执行时间;
expectsCSS 是否受到css影响;
userWantsControl 用户是否想控制css;
cb 定义过渡完成后执行的回调函数;

6、没有 show 属性时,调用 mergeVNodeHookhook 函数合并到 def.data.hook[hookey] 中,⽣成新 的 invoker , createFnInvoker ⽅法;
7、开始执行动画,beforeEnterHook 钩子并将 el 传入;然后判断 expectsCSS ,如果为 true 则表明希望⽤ CSS 来控制动画,那么会执⾏ addTransitionClass(el, startClass) 和 addTransitionClass(el, activeClass)
8、addTransitionClass 主要是给当前 dom 元素 el 添加样式,这里添加的 startClassactiveClass ,就相当于给 name 属性添加了 xxx-enter 和 xxx-enter-active 样式;
9、然后执行 nextFrame() 进入下一帧,下一帧主要是移除掉上一帧增加好的 class;然后判断此时过渡没有被取消,没有则执⾏ addTransitionClass(el, toClass) 添加 toClass;之后如果用户没有通过 enterHook 钩子函数来控制动画,此时若用户指定了 duration 时间,则执行 setTimeout 进行 duration 时长的延时,否则执行 whenTransitionEnds 决定 cb 的执行时机;
10、whenTransitionEnds:通过 getTransitionInfo 获取到 transition 的一些信息,如 type、timeout、propCount,并为 el 元素绑定上 onEnd。随后不停执行下一帧,更新 ended 值,直到动画结束。则将 el 元素的 onEnd 移除,并执行 cb 函数;
11、执行 cb 函数, 把toClassactiveClass 移除,然后判断如果有没有取消,如果取消则移除 startClass 并执⾏ enterCancelledHook ,否则执⾏ afterEnterHook(el)
12、如果有 show 属性,则直接执行enterHook
13、如果不受css影响也不受用户控制则直接执行 cb 函数;

leave

export function leave (vnode: VNodeWithData, rm: Function) {
  const el: any = vnode.elm
  // 如果有进入的回调函数,直接执行
  if (isDef(el._enterCb)) {
    el._enterCb.cancelled = true
    el._enterCb()
  }
  //解析过渡数据
  const data = resolveTransition(vnode.data.transition)
  if (isUndef(data) || el.nodeType !== 1) {
    return rm()
  }
  //el中存在_leaveCb之直接返回
  if (isDef(el._leaveCb)) {
    return
  }
  const {
    css,
    type,
    leaveClass,
    leaveToClass,
    leaveActiveClass,
    beforeLeave,
    leave,
    afterLeave,
    leaveCancelled,
    delayLeave,
    duration
  } = data
  //过渡是否受到css影响
  const expectsCSS = css !== false && !isIE9
  //用户是否想控制CSS
  const userWantsControl = getHookArgumentsLength(leave)
  //动画执行时间
  const explicitLeaveDuration: any = toNumber(
    isObject(duration)
      ? duration.leave
      : duration
  )
  // 获取 leave 动画执行时间
  if (process.env.NODE_ENV !== 'production' && isDef(explicitLeaveDuration)) {
    checkDuration(explicitLeaveDuration, 'leave', vnode)
  }
  //定义过渡完成后执行的回调函数
  const cb = el._leaveCb = once(() => {
    if (el.parentNode && el.parentNode._pending) {
      el.parentNode._pending[vnode.key] = null
    }
    if (expectsCSS) {
      removeTransitionClass(el, leaveToClass)
      removeTransitionClass(el, leaveActiveClass)
    }
    if (cb.cancelled) {
      if (expectsCSS) {
        removeTransitionClass(el, leaveClass)
      }
      leaveCancelled && leaveCancelled(el)
    } else {
      rm()
      afterLeave && afterLeave(el)
    }
    el._leaveCb = null
  })
  
  if (delayLeave) {
    delayLeave(performLeave)
  } else {
    performLeave()
  }

  function performLeave () {
    // the delayed leave may have already been cancelled
    if (cb.cancelled) {
      return
    }
    // record leaving element
    if (!vnode.data.show && el.parentNode) {
      (el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key: any)] = vnode
    }
    beforeLeave && beforeLeave(el)
    if (expectsCSS) {
      addTransitionClass(el, leaveClass)
      addTransitionClass(el, leaveActiveClass)
      nextFrame(() => {
        removeTransitionClass(el, leaveClass)
        if (!cb.cancelled) {
          addTransitionClass(el, leaveToClass)
          if (!userWantsControl) {
            if (isValidDuration(explicitLeaveDuration)) {
              setTimeout(cb, explicitLeaveDuration)
            } else {
              whenTransitionEnds(el, type, cb)
            }
          }
        }
      })
    }
    leave && leave(el, cb)
    if (!expectsCSS && !userWantsControl) {
      cb()
    }
  }
}

leave 的实现和 enter 的实现基本上的一样的,不同的是从 data 中解析出来的是 leave 相关的样式类名和钩子函数,还有一点不同是可以配置 delayLeave ,它是一个函数,可以延时执行 leave 的相关过渡动画,在 leave 动画执行完成之后,它会执行 rm 函数 把节点从 DOM 中移除;

总结

Vue 的过渡实现步骤:
1、自动检测目标元素是否使用了 CSS 过渡动画,如果有则在恰当的时机添加和删除 CSS类名;
2、如果过渡组件提供了 javascript 钩子函数,这些钩子函数将在恰当的时机调用;
3、如果没有 javascript 钩子,也没有 CSS 过渡动画,DOM 操作(插入和删除)在下一帧中立即执行;

所有执行的动画其实是 CSS 或者 javascript 钩子函数提供的,而 transition 只是帮我们管理这些动画,控制 CSS 的添加和删除,把控 javascript 钩子函数执行时机。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值