深入理解Vue transition源码分析
这两天学习了Vuetransition感觉这个地方知识点挺多的,而且很重要,所以,今天添加一点小笔记。
本来打算自己造一个transition的轮子,所以决定先看看源码,理清思路。Vue的transition组件提供了一系列钩子函数,并且具有良好可扩展性。
了解构建过程
既然要看源码,就先让Vue在开发环境跑起来,首先从GitHubclone下来整个项目,在文件./github/CONTRIBUTING.md中看到了如下备注,需要强调一下的是,npmrundev构建的是runtime+compiler版本的Vue。
#watchandautore-builddist/vue.js $npmrundev
紧接着在package.json中找到dev对应的shell语句,就是下面这句
"scripts":{
"dev":"rollup-w-cbuild/config.js--environmentTARGET:web-full-dev",
...
}
Vue2使用rollup打包,-c后面跟的是打包的配置文件(build/config.js),执行的同时传入了一个TARGET参数,web-full-dev。打开配置文件继续往里找。
...
constbuilds={
...
'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的入口了。我们发现在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,顺着这个文件继续往下找。查看src/platforms/web/entry-runtime-with-compiler.js的代码,这里主要是处理将Vue实例挂载到真实dom时的一些异常操作提示,,比如不要把vue实例挂载在body或html标签上等。但是对于要找的transition,这些都不重要,重要的是
importVuefrom'./runtime/index'
Vue对象是从当前目录的runtime文件夹引入的。打开./runtime/index.js,先查看引入了哪些模块,发现Vue是从src/core/index引入的,并看到platformDirectives和platformComponents,官方的指令和组件八九不离十就在这了。
importVuefrom'core/index' ... ... importplatformDirectivesfrom'./directives/index' importplatformComponentsfrom'./components/index' ... //installplatformruntimedirectives&components extend(Vue.options.directives,platformDirectives) extend(Vue.options.components,platformComponents) //installplatformpatchfunction Vue.prototype.__patch__=inBrowser?patch:noop
在platformComponents中发现transtion.js,它export了一个对象,这个对象有name,props和rander方法,一个标准的Vue组件。至此算是找到了源码位置。
exportdefault{
name:'transition',
props:transitionProps,
abstract:true,
render(h:Function){
...
}
}
transition实现分析
从上一节的代码中,可以看到directives和components是保存在Vue.options里面的,还需要注意一下后面的Vue.prototype.patch,因为transtion并不单单是以一个组件来实现的,还需要在Vue构造函数上打一些patch。
rander当中的参数h方法,就是Vue用来创建虚拟DOM的createElement方法,但在此组件中,并没有发现处理过度动画相关的逻辑,主要是集中处理props和虚拟DOM参数。因为transtion并不单单是以一个组件来实现的,它需要操作真实dom(未插入文档流)和虚拟dom,所以只能在Vue的构造函数上打一些patch了。
往回看了下代码,之前有一句Vue.prototype.__patch__=inBrowser?patch:noop,在patch相关的代码中找到了transition相关的实现。modules/transtion.js
这就是过渡动画效果相关的patch的源码位置。
exportfunctionenter(vnode:VNodeWithData,toggleDisplay:?()=>void){
...
}
exportfunctionleave(vnode:VNodeWithData,rm:Function){
...
}
exportdefaultinBrowser?{
create:_enter,
activate:_enter,
remove(vnode:VNode,rm:Function){
/*istanbulignoreelse*/
if(vnode.data.show!==true){
leave(vnode,rm)
}else{
rm()
}
}
}:{}
这个模块默认export的对象包括了三个生命周期函数create,activate,remove,这应该是Vue没有对外暴露的生命周期函数,create和activate直接运行的就是上面的enter方法,而remove执行了leave方法。
继续看最重要的是两个方法,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代替
constraf=inBrowser&&window.requestAnimationFrame
?window.requestAnimationFrame.bind(window)
:setTimeout
exportfunctionnextFrame(fn:Function){
raf(()=>{
raf(fn)
})
}
这种方式的nextFrame实现,正如官方文档中所说的在下一帧添加了toClass,并remove掉startClass,最后在过渡效果结束以后,remove掉了所有的过渡相关class。至此‘进入过渡'的部分完毕。
再来看‘离开过渡'的方法leave,在leave方法中打断点,发现html标签的状态如下
xxx
为vue的占位符,当元素通过v-if隐藏后,会在原来位置留下占位符。那就说明,当leave方法被触发时,原本的真实dom元素已经隐藏掉了(从vnode中被移除),而正在显示的元素,只是一个真实dom的副本。
leave方法关键代码其实和enter基本一致,只不过是将startClass换为了leaveClass等,还有处理一些动画生命周期的勾子函数。在动画结束后,调用了由组件生命周期remove传入的rm方法,把这个dom元素的副本移出了文档流。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。