vue-transition原理分析(二)

1,768 阅读4分钟

​ 在上一篇博文中,我们知道vue中的transition是一个抽象组件,其功能是给子组件的data上绑定了transition属性,而这个transition属性是由绑定在transition组件上的属性和事件组合而成,也就是说把绑定在transition组件上的属性和事件透传到子组件上。本篇文章我们一起来看一下子组件上的transition属性是如何发挥作用的。

一、transition module

​ vue的源码分为核心部分(core目录)和平台部分(platforms部分),核心部分代码提供一致的与平台无关的能力,平台部分代码则是根据平台自身能力的不同提供相应的实现(目前已有的两个平台是web和weex),这样的实现减少了代码的耦合性,而且更便于后续兼容其他平台。

​ core/vdom/path.j中提供了createPatchFunction方法供平台侧调用,该方法的作用是根据平台侧传入的配置生成最终环境上实际使用的patch方法。在core/vdom/path.j中定义了组件渲染的5个钩子,在渲染的对应阶段会调用注册的钩子,钩子是通过modules中定义的。

// vue源码/src/core/vdom/patch.js

// 组件渲染的钩子
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend
  
	// 从modules中提取各阶段的hooks
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  // 省略其他代码
  ......
}

​ 在platforms/web/runtime/patch.js中调用了createPatchFunction方法

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

​ 可以看到将platformModules和baseModules组合成新的modeuls后传入createPatchFunction,platformModules中是web平台下相应的modules实现,包括transition module。

// vue源码/src/platforms/web/runtime/modules/trainsition.js

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()
    }
  }
} : {}

​ modules/trainsition.js中定义了3个钩子create、active、remove,其中create、active都是调用_enter方法,remove是根调用leave方法。

二、_enter方法

​ 看一下_enter方法的实现:

// vue源码/src/platforms/web/runtime/modules/trainsition.js

function _enter (_: any, vnode: VNodeWithData) {
  // 若当前节点未展示,调用enter方法
  if (vnode.data.show !== true) {
    enter(vnode)
  }
}

export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
  const el: any = vnode.elm
	
  // 从vode.data.transition中解析数据
  const data = resolveTransition(vnode.data.transition)

  // 数据解构赋值
  const {
    css,
    type,
    enterClass,
    enterToClass,
    enterActiveClass,
    appearClass,
    appearToClass,
    appearActiveClass,
    beforeEnter,
    enter,
    afterEnter,
    enterCancelled,
    beforeAppear,
    appear,
    afterAppear,
    appearCancelled,
    duration
  } = data

  // activeInstance will always be the <transition> component managing this
  // transition. One edge case to check is when the <transition> is placed
  // as the root node of a child component. In that case we need to check
  // <transition>'s parent for appear check.
  let context = activeInstance
  let transitionNode = activeInstance.$vnode
  while (transitionNode && transitionNode.parent) {
    context = transitionNode.context
    transitionNode = transitionNode.parent
  }
  
	// 是否是初次渲染
  const isAppear = !context._isMounted || !vnode.isRootInsert
	
  // 是初次渲染,并且appear值为false时,直接返回
  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
  )
	
  // css=false表明仅使用javascript过渡,vue会跳过css的监测
  const expectsCSS = css !== false && !isIE9
  // enter事件的响应函数,其第二个参数:回调函数done是可选的
  const userWantsControl = getHookArgumentsLength(enterHook)
	
  // enter后的回调
  const cb = el._enterCb = once(() => {
    if (expectsCSS) {
      // 如果是css过渡
      removeTransitionClass(el, toClass)
      removeTransitionClass(el, activeClass)
    }
    if (cb.cancelled) {
      // 如果enter回调被取消(会在leave方法中取消)
      if (expectsCSS) {
        removeTransitionClass(el, startClass)
      }
      enterCancelledHook && enterCancelledHook(el)
    } else {
      afterEnterHook && afterEnterHook(el)
    }
    el._enterCb = null
  })

  // start enter transition
  beforeEnterHook && beforeEnterHook(el)
  
  // 执行css过渡
  if (expectsCSS) {
    addTransitionClass(el, startClass)
    addTransitionClass(el, activeClass)
    // 下一帧
    nextFrame(() => {
      removeTransitionClass(el, startClass)
      if (!cb.cancelled) {
        addTransitionClass(el, toClass)
        if (!userWantsControl) {
          if (isValidDuration(explicitEnterDuration)) {
            // 设置了显示持续时间的,用定时器调用cb
            setTimeout(cb, explicitEnterDuration)
          } else {
            // 没有设置持续时间的,根据type判断过渡结束时调用cb
            whenTransitionEnds(el, type, cb)
          }
        }
      }
    })
  }

  if (vnode.data.show) {
    toggleDisplay && toggleDisplay()
    enterHook && enterHook(el, cb)
  }
	
  if (!expectsCSS && !userWantsControl) {
    cb()
  }
}

​ enter代码的逻辑还是比较清晰的,从vnode.data.transition中取到数据,然后根据是否初次渲染、是否CSS过渡、是否用户控制enter done、是否定义显示持续时间等条件进行处理。对于CSS过渡的,通过在适当的时机添加和移除transition class实现,对于js过渡的,只需要在适当的时间调用绑定注册的事件回调即可。

​ leave方法与enter方法的实现基本一致,这里不再详细讨论,有兴趣的可以自己去看一下源码。

三、总结

​ vue核心代码在组件渲染时提供了钩子,平台代码中实现了transition module,注册了create、activate、remove等钩子函数。在组件渲染时读取vnode.data.transition的数据,根据transition的数据采用css过渡或者js过渡,从而实现过渡效果。