原本打算本身造一个transition的轮子,因此决定先看看源码,理清思路。Vue的transition组件提供了一系列钩子函数,而且具备良好可扩展性。javascript
既然要看源码,就先让Vue在开发环境跑起来,首先从GitHub clone下来整个项目,在文件./github/CONTRIBUTING.md
中看到了以下备注,须要强调一下的是,npm run dev构建的是runtime + compiler版本的Vue。html
# watch and auto re-build dist/vue.js $ npm run dev
紧接着在package.json中找到dev对应的shell语句,就是下面这句vue
"scripts": { "dev": "rollup -w -c build/config.js --environment TARGET:web-full-dev", ... }
Vue2使用rollup打包,-c 后面跟的是打包的配置文件(build/config.js),执行的同时传入了一个TARGET参数,web-full-dev。打开配置文件继续往里找。java
... 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的入口了。 咱们发如今Vue的根目录下并无web这个文件夹,其实是由于Vue给path.resolve这个方法加了个alias, alias的配置在/build/alias.js中node
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,这些都不重要,重要的是git
import Vue from './runtime/index'
Vue对象是从当前目录的runtime文件夹引入的。打开./runtime/index.js,先查看引入了哪些模块, 发现Vue是从src/core/index引入的,并看到platformDirectives和platformComponents,官方的指令和组件八九不离十就在这了。github
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
在platformComponents中发现transtion.js,它export了一个对象,这个对象有name,props和rander方法,一个标准的Vue组件。至此算是找到了源码位置。web
export default { name: 'transition', props: transitionProps, abstract: true, render (h: Function) { ... } }
从上一节的代码中,能够看到directives和components是保存在Vue.options里面的, 还须要注意一下后面的Vue.prototype.patch,由于transtion并不仅仅是以一个组件来实现的,还须要在Vue构造函数上打一些patch。shell
rander当中的参数h方法,就是Vue用来建立虚拟DOM的createElement方法,但在此组件中,并无发现处理过分动画相关的逻辑,主要是集中处理props和虚拟DOM参数。由于transtion并不仅仅是以一个组件来实现的,它须要操做真实dom(未插入文档流)和虚拟dom,因此只能在Vue的构造函数上打一些patch了。npm
往回看了下代码,以前有一句Vue.prototype.__patch__ = inBrowser ? patch : noop
,在patch相关的代码中找到了transition相关的实现。modules/transtion.js
这就是过渡动画效果相关的patch的源码位置。
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() } } } : {}
这个模块默认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代替
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元素的副本移出了文档流。
若有错误,欢迎指正。
这篇并无去分析Vue core相关的内容,推荐一篇讲Vue core很是不错的文章,对Vue构造函数如何来的感兴趣的同窗能够看这里