從去年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仍是故意這樣設計的)。