Vue2.0 介绍
1.0版本正式发布后,就在业务中开始使用,将原先jQuery的功能逐步的进行迁移。16年10月1日,Vue2.0版本正式发布了,其中核心代码都进行了重写,于是就专门花时间,对Vue2.0的源码进行了学习。本篇文章就是2.0源码学习的总结。
先对Vue2.0的新特性做一个简单的介绍:
大小 & 性能:Vue2.0的线上包gzip后只有12kb,而1.0需要22kb,react需要44kb。而且Vue2.0的性能在react等几个框架中,性能是最快的。
VDOM:实现了virtual dom,并且将静态子树进行了提取,减少界面重绘时的对比。与1.0对比性能有明显提升。
Server Render:2.0还对Server Render做了支持。
Vue的最新源码可以去https://github.com/vuejs/vue获得。本文讲的是2.0.3版本,2.0.3可以去https://github.com/vuejs/vue/tree/v2.0.3这里获得。
下面开始进入正题,首先从生命周期开始。
生命周期
上图就是官方给出的Vue 2.0的生命周期图,其中包含了Vue对象生命周期过程中的几个核心步骤。了解这几个过程,可以很好帮助我们理解Vue的创建与销毁过程。从图中我们可以看出,生命周期分为4个周期:
create:
new Vue
时,会先进行create,创建出Vue对象。mount:根据
el,template,render
方法等属性,会生成DOM,并添加到对应位置。update:当数据发生变化后,会重新渲染DOM,并进行替换。
destroy:销毁时触发。
那么这4个过程在源码中是怎么实现的?我们从new Vue
中开始。
new Vue
为了更好的理解new的过程,我整理了一个序列图:
new Vue的过程主要涉及到三个对象:vm、compiler、watcher
。其中,vm
表示Vue
的具体对象;compiler
负责将template
解析为AST render
方法;watcher
用于观察数据变化,以实现数据变化后进行re-render
。
下面来分析下具体过程和代码:
首先,运行new Vue()
的时候,会进入代码src/core/instance/index.js
的Vue构造函数中,并执行this._init()
方法。在_init
中,会对各个功能进行初始化,并执行beforeCreate
和created
两个生命周期方法。核心代码如下:
1 | initLifecycle(vm) |
这个过程有一点需要注意:
beforeCreate
和create
之间只有initState
,和官方给出的生命周期并不完全一样。这里的initState
是用于初始化data,props
等的监听的。
在init
的最后,会运行initRender
方法。在该方法中,会运行vm.$mount
方法,代码如下:
1 | if (vm.$options.el) { |
这里的
vm.$mount
可以在业务代码中调用,这样,new
过程和mount
过程就可以根据业务情况进行分离。
这里的$mount
在src/entries/web-runtime-with-compiler.js
中,主要逻辑是根据el,template,render
三个属性来获得AST render
方法。代码如下:
1 | if (!options.render) { // 如果有render方法,直接运行mount |
这个过程有三点需要注意:
1、compile时,将最大的静态子树提取出来作为单独的AST渲染方法,以提升后面vNode对比时的性能。所以,当存在多个连续的静态标签时,可以在外边添加一个静态父节点,这样,staticRenderFns数目可以减少,从而提升性能。
2、Vue2.0 中的模板有三种引用方法:el,template,render(JSX)。其中优先级是render > template > el。
3、el,template两种写法,最后都会通过compiler转化为render(JSX)来运行,也就是说,直接写成render(JSX)的性能是最佳的。当然,如果使用了构建工具,最终生成的包就是使用的render(JSX)。这样子,在源码上就可以不用过多考虑这一块的性能了,直接用可维护性最好的方式就行。
将模板转化为render,用到了compileToFunction
方法,该方法最后会通过src/compiler/index.js
文件中的compile
方法,将模板转化为AST语法结构的render方法,并对静态子树进行分离。
完成render
方法的生成后,会进入_mount(src/core/instance/lifecycle.js)
中进行DOM更新。该方法的核心逻辑如下:
1 | vm._watcher = new Watcher(vm, () => { |
首先会new一个watcher对象,在watcher对象创建后,会运行传入的方法vm._update(vm._render(), hydrating)
(watcher的逻辑在下面的watcher小节中细讲)。其中的vm._render()
主要作用就是运行前面的compile生成的render方法,并返回一个vNode对象。这里的vNode就是一个虚拟的DOM节点。
拿到vNode后,传入 vm._update()
方法,进行DOM更新。
VDOM
上面已经讲完了new Vue
过程中的主要步骤,其中涉及到template如何转化为DOM的过程,这里单独拿出来讲下。先上序列图:
从图中可以看出,从template到DOM,有三个过程:
template -> AST render (compiler解析template)
AST render -> vNode (render方法运行)
vNode -> DOM (vdom.patch)
首先template在compiler中解析为AST render方法的过程。上一节中有说到,initState
后,会调用src/entries/web-runtime-with-compiler.js
中的Vue.prototype.$mount
方法。在$mount
中,会获取template,然后调用src/platforms/web/compiler/index.js
的compilerToFunction
方法。在该方法中,会运行compile将template解析为多个render方法,也就是AST render。这里的compile在文件src/compiler/index.js
中,代码如下:
1 | const ast = parse(template.trim(), options) // 解析template为AST |
可以看出,compile方法就是将template以AST的方式进行解析,并转化为render方法进行返回。
再看第二个过程,AST render -> vNode。这个过程很简单,就是将AST render方法进行运行,获得返回的vNode对象。
最后一步,vNode -> DOM。该过程中,存在vNode的对比以及DOM的添加修改操作。在上一节中,有讲到vm._update()
方法中对DOM进行更新。_update
的主要代码如下:
1 | // src/core/instance/lifecycle.js |
可以看出,无论是首次添加还是后期的update,都是通过__patch__
来更新的。这里的__patch__
核心步骤是在src/core/vdom/patch.js
中的patch方法进行实现,源码如下:
1 | function patch (oldVnode, vnode, hydrating, removeOnly) { |
首先添加很简单,就是通过insertBefore
将转换好的element添加到DOM中。如果update,则会调动patchVNode()
。最后来看下patchVNode
的代码:
1 | function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { |
其中有调用了updateChildren
来更新子节点,代码如下:
1 | function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { |
可以看到updateChildren
中,又通过patchVnode
来更新当前节点。梳理一下,patch
通过patchVnode
来更新根节点,然后通过updateChildren
来更新子节点,具体子节点,又通过patchVnode
来更新,通过一个类似于递归的方式逐个节点的完成对比和更新。
Vue 2.0中如何去实现VDOM的思路是否清晰,通过4层结构,很好的实现了可维护性,也为实现server render,weex等功能提供了可能。拿server render举例,只需要将最后
vNode -> DOM
改成vNode -> String
或vNode -> Stream
,就可以实现server render。剩下的compiler和Vue核心逻辑都不需要改。
Watcher
我们都知道MVVM框架的特征就是当数据发生变化后,会自动更新对应的DOM节点。使用MVVM之后,业务代码中就可以完成不写DOM操作代码,不仅可以将业务代码聚焦在业务逻辑上,还可以提供业务的可维护性和可测试性。那么Vue2.0中是怎么实现对数据变化的监听呢?照例,先看序列图:
可以看出,整个Watcher的过程可以分为三个过程。
对state设置setter/getter
对vm设置好watcher,添加好state触发setter时的排序方法
state变化触发执行
在前面有说过,在生命周期函数beforeCreate
和created
直接,会运行方法initState()
。在initState
中,会对props,data,computed等属性添加setter/getter
。拿data举例,设置setter/getter
的代码如下:
1 | function initData (vm: Component) { |
通过调用observe
方法,会对data添加好观察者,核心代码为:
1 | Object.defineProperty(obj, key, { |
这个时候,对data的监听已经完成。可以看到,当data发生变化时,会运行dep.notify()。在notify方法中,会去运行watcher的update方法,内容如下:
1 | update () { |
update
方法中,queueWatcher
方法的目的是通过 nextTicker
来执行 run
方法,属于支线逻辑,就不分析了,这里直接看 run
的实现。run
方法其实很简单,就是简单的调用 get
方法,而 get
方法会通过执行 this.getter()
来更新DOM。
那么this.getter
是什么呢?本文最开始分析new Vue
过程时,有讲到运行_mount
方法时,会运行如下代码:
1 | vm._watcher = new Watcher(vm, () => { |
那么this.getter
就是这里Watcher方法的第二参数。来看下new Watcher
的代码:
1 | export default class Watcher { |
可以看出,在new Vue过程中,Watcher会在构造完成后主动调用this.get()来触发this.getter()方法的运行,以达到更新DOM节点。
总结一下这个过程:首先_init时,会对Data设置好setter方法,setter方法中会调用dep.notify(),以便数据变化时通知DOM进行更新。然后new Watcher时,会更新DOM的方法进行设置,也就是Watcher.getter方法。最后,当Data发生变化时候,dep.notify()运行,运行到watcher.getter()时,就会去运行render和update逻辑,最终达到DOM更新的目的。
总结和收获
刚开始觉得看源码,是因为希望能了解下Vue 2.0的实现,看看能不能得到一些从文档中无法知道的细节,用于提升运行效率。把主要流程理清楚后,的确了解到一些,这里做个整理:
el属性传入的如果不是element,最后会通过
document.querySelector
来获取的,这个接口性能较差,所以,el传入一个element性能会更好。$mount
方法中对html,body标签做了过滤,这两个不能用来作为渲染的根节点。每一个组件都会从
_init
开始重新运行,所以,当存在一个长列表时,将子节点作为一个组件,性能会较差。*.vue
文件会在构建时转化为render方法,而render方法的性能比指定template更好。所以,源码使用*.vue
的方式,性能会更好。如果需要自定义
delimiters
,每一个组件都需要单独指定。如果是
*.vue
文件,制定delimiters
是失效的,因为vue-loader
对*.vue
文件进行解析时,并没有将delimiters
传递到compiler.compile()
中。
转自:https://segmentfault.com/a/1190000007484936
结构图
下面是取自通过一张图走进 Vue 2.0的Vue2.0的结构图
参考资料