从去年9月份了解到Vue后,就被他简洁的API所吸引。1.0版本正式发布后,就在业务中开始使用,将原先jQuery的功能逐步的进行迁移。
今年的10月1日,Vue的2.0版本正式发布了,其中核心代码都进行了重写,因而就专门花时间,对Vue 2.0的源码进行了学习。本篇文章就是2.0源码学习的总结。 javascript
先对Vue 2.0的新特性作一个简单的介绍:html
大小 & 性能。Vue 2.0的线上包gzip后只有12Kb,而1.0须要22Kb,react须要44Kb。并且,Vue 2.0的性能在react等几个框架中,性能是最快的。vue
VDOM。实现了Virtual DOM, 而且将静态子树进行了提取,减小界面重绘时的对比。与1.0对比性能有明显提高。java
template & JSX。众所周知,Vue 1.0使用的是template来实现模板,而React使用了JSX实现模板。关于template和JSX的争论也不少,不少人不使用React就是由于没有支持template写法。Vue 2.0对template和JSX写法都作了支持。使用时,能够根据具体业务细节进行选择,能够很好的发挥二者的优点。就这一点,Vue已经超过React了。node
Server Render。2.0还对了Server Render作了支持。这一点并无在业务中使用,不作评价。react
Vue的最新源码能够去 https://github.com/vuejs/vue 得到。本文讲的是 2.0.3版本,2.0.3能够去 https://github.com/vuejs/vue/... 这里得到。 git
下面开始进入正题。首先从生命周期开始。github
上图就是官方给出的Vue 2.0的生命周期图,其中包含了Vue对象生命周期过程当中的几个核心步骤。了解了这几个过程,能够很好的帮助咱们理解Vue的建立与销毁过程。
从图中咱们能够看出,生命周期主要分为4个过程:web
create。new Vue
时,会先进行create,建立出Vue对象。weex
mount。根据el, template, render方法等属性,会生成DOM,并添加到对应位置。
update。当数据发生变化后,会从新渲染DOM,并进行替换。
destory。销毁时运行。
那么这4个过程在源码中是怎么实现的呢?咱们从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
两个生命周期方法。核心代码以下:
initLifecycle(vm) initEvents(vm) callHook(vm, 'beforeCreate') initState(vm) callHook(vm, 'created') initRender(vm)
这个过程有一点须要注意:
beforeCreate和created之间只有initState,和官方给出的生命周期图并不彻底同样。这里的initState是用于初始化data,props等的监听的。
在_init
的最后,会运行initRender
方法。在该方法中,会运行vm.$mount
方法,代码以下:
if (vm.$options.el) { vm.$mount(vm.$options.el) }
这里的
vm.$mount
能够在业务代码中调用,这样,new 过程和 mount过程就能够根据业务状况进行分离。
这里的$mount
在src/entries/web-runtime-with-compiler.js
中,主要逻辑是根据el, template, render三个属性来得到AST render方法。代码以下:
if (!options.render) { // 若是有render方法,直接运行mount let template = options.template if (template) { // 若是有template, 获取template参数对于的HTML做为模板 if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { // 若是没有template, 且存在el,则获取el的outerHTML做为模板 template = getOuterHTML(el) } if (template) { // 若是获取到了模板,则将模板转化为render方法 const { render, staticRenderFns } = compileToFunctions(template, { warn, shouldDecodeNewlines, delimiters: options.delimiters }, this) options.render = render options.staticRenderFns = staticRenderFns } } return mount.call(this, el, hydrating)
这个过程有三点须要注意:
compile时,将最大静态子树提取出来做为单独的AST渲染方法,以提高后面vNode对比时的性能。因此,当存在多个连续的静态标签时,能够在外边添加一个静态父节点,这样,staticRenderFns数目能够减小,从而提高性能。
Vue 2.0中的模板有三种引用写法:el, template, render(JSX)。其中的优先级是render > template > el。
el, template两种写法,最后都会经过compiler转化为render(JSX)来运行,也就是说,直接写成render(JSX)是性能最优的。固然,若是使用了构建工具,最终生成的包就是使用的render(JSX)。这样子,在源码上就能够不用过多考虑这一块的性能了,直接用可维护性最好的方式就行。
将模板转化为render,用到了compileToFunctions方法
,该方法最后会经过src/compiler/index.js
文件中的compile
方法,将模板转化为AST语法结构的render方法,并对静态子树进行分离。
完成render方法的生成后,会进入_mount
(src/core/instance.lifecycle.js)中进行DOM更新。该方法的核心逻辑以下:
vm._watcher = new Watcher(vm, () => { vm._update(vm._render(), hydrating) }, noop)
首先会new一个watcher对象,在watcher对象建立后,会运行传入的方法vm._update(vm._render(), hydrating)
(watcher的逻辑在下面的watcher小节中细讲)。其中的vm._render()
主要做用就是运行前面compiler生成的render方法,并返回一个vNode对象。这里的vNode就是一个虚拟的DOM节点。
拿到vNode后,传入vm._update()
方法,进行DOM更新。
上面已经讲完了new Vue
过程当中的主要步骤,其中涉及到template如何转化为DOM的过程,这里单独拿出来说下。先上序列图:
从图中能够看出,从template到DOM,有三个过程:
template -> AST render (compiler解析template)
AST render -> vNode (render方法运行)
vNode -> DOM (vdom.patch)
首先是template在compiler中解析为AST render方法的过程。上一节中有说到,initRender
后,会调用到src/entries/web-runtime-with-compiler.js
中的Vue.prototype.$mount
方法。在$mount
中,会获取template,而后调用src/platforms/web/compiler/index.js
的compileToFunctions
方法。在该方法中,会运行compile
将template解析为多个render方法,也就是AST render。这里的compile
在文件src/compiler/index.js
中,代码以下:
const ast = parse(template.trim(), options) // 解析template为AST optimize(ast, options) // 提取static tree const code = generate(ast, options) // 生成render 方法 return { ast, render: code.render, staticRenderFns: code.staticRenderFns }
能够看出,compile
方法就是将template以AST的方式进行解析,并转化为render方法进行返回。
再看第二个过程:AST render -> vNode。这个过程很简单,就是将AST render方法进行运行,得到返回的vNode对象。
最后一步,vNode -> DOM。该过程当中,存在vNode的对比以及DOM的添加修改操做。
在上一节中,有讲到vm._update()
方法中对DOM进行更新。_update
的主要代码以下:
// src/core/instance/lifecycle.js if (!prevVnode) { // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. vm.$el = vm.__patch__(vm.$el, vnode, hydrating) // 首次添加 } else { vm.$el = vm.__patch__(prevVnode, vnode) // 数据变化后触发的DOM更新 }
能够看出,不管是首次添加仍是后期的update,都是经过__patch__
来更新的。这里的__patch__
核心步骤是在src/core/vdom/patch.js
中的patch
方法进行实现,源码以下:
function patch (oldVnode, vnode, hydrating, removeOnly) { if (!oldVnode) { ... } else { ... if (!isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) // diff并更新DOM。 } else { elm = oldVnode.elm parent = nodeOps.parentNode(elm) ... if (parent !== null) { nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm)) // 添加element到DOM。 removeVnodes(parent, [oldVnode], 0, 0) } ... } } ... }
首次添加很简单,就是经过insertBefore将转化好的element添加到DOM中。若是是update,则会调动patchVnode()
。最后来看下patchVnode
的代码:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { ... const elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children const ch = vnode.children ... if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // 当都存在时,更新Children if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // 只存在新节点时,即添加节点 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 只存在老节点时,即删除节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // 删除了textContent nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 修改了textContent nodeOps.setTextContent(elm, vnode.text) } }
其中有调用了updateChildren
来更新子节点,代码以下:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { ... while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { ... } } ... }
能够看到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的核心逻辑都不须要改。
咱们都知道MVVM框架的特征就是当数据发生变化后,会自动更新对应的DOM节点。使用MVVM以后,业务代码中就能够彻底不写DOM操做代码,不只能够将业务代码聚焦在业务逻辑上,还能够提升业务代码的可维护性和可测试性。那么Vue 2.0中是怎么实现对数据变化的监听的呢?照例,先看序列图:
能够看出,整个Watcher的过程能够分为三个过程。
对state设置setter/getter
对vm设置好Watcher,添加好state 触发 setter时的执行方法
state变化触发执行
前面有说过,在生命周期函数beforeCreate
和created
直接,会运行方法initState()
。在initState
中,会对Props, Data, Computed等属性添加Setter/Getter。拿Data举例,设置setter/getter的代码以下:
function initData (vm: Component) { let data = vm.$options.data ... // proxy data on instance const keys = Object.keys(data) let i = keys.length while (i--) { ... proxy(vm, keys[i]) // 设置vm._data为代理 } // observe data observe(data) }
经过调用observe
方法,会对data添加好观察者,核心代码为:
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() // 处理好依赖watcher ... } return value }, set: function reactiveSetter (newVal) { ... childOb = observe(newVal) // 对新数据从新observe dep.notify() // 通知到dep进行数据更新 } })
这个时候,对data的监听已经完成。能够看到,当data发生变化的时候,会运行dep.notify()
。在notify
方法中,会去运行watcher的update
方法,内容以下:
update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } run () { if (this.active) { const value = this.get() } ... }
update
方法中,queueWatcher方法的目的是经过nextTicker
来执行run
方法,属于支线逻辑,就不分析了,这里直接看run
的实现。run
方法其实很简单,就是调用get
方法,而get
方法会经过执行this.getter()
来更新DOM。
那么this.getter
是什么呢?本文最开始分析new Vue
过程时,有讲到运行_mount
方法时,会运行以下代码:
vm._watcher = new Watcher(vm, () => { vm._update(vm._render(), hydrating) }, noop)
那么this.getter
就是这里Watcher方法的第二个参数。来看下new Watcher
的代码:
export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object = {} ) { ... if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) } ... this.value = this.lazy ? undefined : this.get() } }
能够看出,在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()
中。(这一点不肯定是bug仍是故意这样设计的)。