Vue transition源码分析
本来打算自己造一个transition的轮子,所以决定先看看源码,理清思路。Vue的transition组件提供了一系列钩子函数,并且有良好可扩展性,非常不错。
既然要看源码,就先让整个项目跑起来,首先从GitHub clone下来整个项目,在文件夹./github/CONTRIBUTING.md中找到这句,看来开发环境就是用它啦!(已省略npm install)
# watch and auto re-build dist/vue.js
$ npm run dev
紧接着在package.json中找到dev对应的shell语句,就是下面这句
"scripts": {
"dev": "rollup -w -c build/config.js --environment TARGET:web-full-dev",
...
}
Vue2使用rollup打包,-c 后面跟的肯定就是打包的配置文件(build/config.js),同时传入了一个参数,重点在web-full-dev。现在就可以打开build/config.js了。
...
const builds = {
...
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
...
}
从上面的构建配置中,找到了构建入口web/entry-runtime-with-compiler.js,它也就是umd版本的vue-runtime + template-compiler的入口了。我们发现在Vue的根目录下并没有web这个文件夹,实际上是因为Vue给path.resolve这个方法加了个alias, alias的配置在/build/alias.js中
module.exports = {
vue: path.resolve(__dirname, '../src/platforms/web/entry-runtime-with-compiler'),
compiler: path.resolve(__dirname, '../src/compiler'),
core: path.resolve(__dirname, '../src/core'),
shared: path.resolve(__dirname, '../src/shared'),
web: path.resolve(__dirname, '../src/platforms/web'),
weex: path.resolve(__dirname, '../src/platforms/weex'),
server: path.resolve(__dirname, '../src/server'),
entries: path.resolve(__dirname, '../src/entries'),
sfc: path.resolve(__dirname, '../src/sfc')
}
web对应的目录为’../src/platforms/web’,也就是src/platforms/web了,不要忘了我们在找官方的transition组件,顺着这个文件继续往下找。在src/platforms/web/entry-runtime-with-compiler.js中,这个文件中有很多warn,主要是处理将Vue实例挂载到真实dom时的一些异常操作,比如不要挂载在body或html标签上的提示。但是对于transition,这些都不重要,重要的是
import Vue from './runtime/index'
Vue对象是从当前目录的runtime文件夹引入的。打开./runtime/index.js,先查看引入了哪些模块, 发现Vue是从src/core/index引入的,并看到platformDirectives和platformComponents,说明官方的指令和组件八九不离十就在这了。打开./components,发现文件transtion.js,就是它了。推荐一篇讲Vue core非常不错的文章,对Vue构造函数如何来的感兴趣的同学可以看这里
import Vue from 'core/index'
...
...
import platformDirectives from './directives/index'
import platformComponents from './components/index'
...
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
到这里我们还知道了,directives和components是保存在Vue.options里面的, 还需要注意一下后面的Vue.prototype.patch。
在transtion.js中export了一个对象,这个对象有name,props和rander方法,一个标准的Vue组件。rander当中的参数h方法,就是Vue用来创建虚拟DOM的createElement方法,但在此组件中,并没有发现处理过度动画相关的逻辑,主要是集中处理props和虚拟DOM参数。其实在处理动画的逻辑是写在Vue.prototype.patch中的,因为过渡效果需要操作真实dom和虚拟dom,需要在组件的生命周期内加一些patch。
export default {
name: 'transition',
props: transitionProps,
abstract: true,
render (h: Function) {
...
}
}
打开components同级目录的modules/transtion.js,这就是过渡动画效果相关的patch的源码位置。export default出一个对象,其中包括三个生命周期函数create,activate,remove,这应该是Vue没有对外暴露的生命周期函数,create和activate直接运行的就是上面的enter方法,而remove执行了leave方法。
export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
...
}
export function leave (vnode: VNodeWithData, rm: Function) {
...
}
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()
}
}
} : {}
继续看最重要的是两个方法,enter和leave。通过在这两个方法上打断点得知,执行这两个方法的之前,vnode已经创建了真实dom, 并挂载到了vnode.elm上。其中这段代码比较关键
// el就是真实dom节点
beforeEnterHook && beforeEnterHook(el)
if (expectsCSS) {
addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
nextFrame(() => {
addTransitionClass(el, toClass)
removeTransitionClass(el, startClass)
if (!cb.cancelled && !userWantsControl) {
if (isValidDuration(explicitEnterDuration)) {
setTimeout(cb, explicitEnterDuration)
} else {
whenTransitionEnds(el, type, cb)
}
}
})
}
首先给el添加了startClass和activeClass, 此时dom节点还未插入到文档流,推测应该是在create或activate勾子执行完以后,该节点被插入文档流的。nextFrame方法的实现如下, 如requestAnimationFrame不存在,用setTimeout代替
const raf = inBrowser && window.requestAnimationFrame
? window.requestAnimationFrame.bind(window)
: setTimeout
export function nextFrame (fn: Function) {
raf(() => {
raf(fn)
})
}
这种方式的nextFrame实现,正如官方文档中所说的在下一帧添加了toClass,并remove掉startClass,最后在过渡效果结束以后,remove掉了所有的过渡相关class。至此‘进入过渡’的部分完毕。
再来看‘离开过渡’的方法leave,在leave方法中打断点,发现html标签的状态如下
< p>xxx< /p>
< !---->
为vue的占位符,当元素通过v-if隐藏后,会在原来位置留下占位符。那就说明,当leave方法被触发时,原本的真实dom元素已经隐藏掉了(从vnode中被移除),而正在显示的元素,只是一个真实dom的副本。
leave方法关键代码其实和enter基本一致,只不过是将startClass换为了leaveClass等,还有处理一些动画生命周期的勾子函数。在动画结束后,调用了由组件生命周期remove传入的rm方法,把这个dom元素的副本移出了文档流。
如有错误,欢迎指正。