这两天学习了 vue transition 感觉这个地方知识点挺多的,而且很重要,所以,今天添加一点小笔记。
本来打算自己造一个 transition 的轮子,所以决定先看看源码,理清思路。Vue 的 transition 组件提供了一系列钩子函数,并且具有良好可扩展性。
了解构建过程
既然要看源码,就先让 Vue 在开发环境跑起来,首先从 GitHub clone 下来整个项目,在文件./github/CONTRIBUTING.md 中看到了如下备注,需要强调一下的是,npm run dev 构建的是 runtime + compiler 版本的 Vue。
- # 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),执行的同时传入了一个 TARGET 参数,web-full-dev。打开配置文件继续往里找。
- ...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 中
- 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,这些都不重要,重要的是
- import Vue from './runtime/index'
Vue 对象是从当前目录的 runtime 文件夹引入的。打开./runtime/index.js,先查看引入了哪些模块, 发现 Vue 是从 src/core/index 引入的,并看到 platformDirectives 和 platformComponents,官方的指令和组件八九不离十就在这了。
- 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 组件。至此算是找到了源码位置。
- export default {
- 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 的源码位置。
- 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 元素的副本移出了文档流。
来源: http://www.jb51.net/article/119915.htm