在上一篇博文中,我们知道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过渡,从而实现过渡效果。