摘要:今年的月日,的版本正式發布了,其中核心代碼都進行了重寫,于是就專門花時間,對的源碼進行了學習。本篇文章就是源碼學習的總結。實現了并且將靜態子樹進行了提取,減少界面重繪時的對比。的最新源碼可以去獲得。
Vue2.0介紹
從去年9月份了解到Vue后,就被他簡潔的API所吸引。1.0版本正式發布后,就在業務中開始使用,將原先jQuery的功能逐步的進行遷移。
今年的10月1日,Vue的2.0版本正式發布了,其中核心代碼都進行了重寫,于是就專門花時間,對Vue 2.0的源碼進行了學習。本篇文章就是2.0源碼學習的總結。
先對Vue 2.0的新特性做一個簡單的介紹:
大小 & 性能。Vue 2.0的線上包gzip后只有12Kb,而1.0需要22Kb,react需要44Kb。而且,Vue 2.0的性能在react等幾個框架中,性能是最快的。
VDOM。實現了Virtual DOM, 并且將靜態子樹進行了提取,減少界面重繪時的對比。與1.0對比性能有明顯提升。
template & JSX。眾所周知,Vue 1.0使用的是template來實現模板,而React使用了JSX實現模板。關于template和JSX的爭論也很多,很多人不使用React就是因為沒有支持template寫法。Vue 2.0對template和JSX寫法都做了支持。使用時,可以根據具體業務細節進行選擇,可以很好的發揮兩者的優勢。就這一點,Vue已經超過React了。
Server Render。2.0還對了Server Render做了支持。這一點并沒有在業務中使用,不做評價。
Vue的最新源碼可以去 https://github.com/vuejs/vue 獲得。本文講的是 2.0.3版本,2.0.3可以去 https://github.com/vuejs/vue/... 這里獲得。
下面開始進入正題。首先從生命周期開始。
生命周期上圖就是官方給出的Vue 2.0的生命周期圖,其中包含了Vue對象生命周期過程中的幾個核心步驟。了解了這幾個過程,可以很好的幫助我們理解Vue的創建與銷毀過程。
從圖中我們可以看出,生命周期主要分為4個過程:
create。new Vue時,會先進行create,創建出Vue對象。
mount。根據el, template, render方法等屬性,會生成DOM,并添加到對應位置。
update。當數據發生變化后,會重新渲染DOM,并進行替換。
destory。銷毀時運行。
那么這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兩個生命周期方法。核心代碼如下:
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更新。
VDOM上面已經講完了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來更新,通過一個類似于遞歸的方式逐個節點的完成對比和更新。
WatcherVue 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還是故意這樣設計的)。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/80906.html
摘要:源碼版本為原文地址和有必要了解這兩個概念的區別。點開目錄下的,發現確實是導出了一個構造函數。再回過頭看,它給構造函數擴展了一些方法具體的邏輯后文看。 前言 網上vue的源碼分析也蠻多的,不過很多都是1.0版本的并且大多都是在講數據的observe,索性自己看看源碼,雖然很難但是希望能學到點東西。 源碼版本為2.0.0 原文地址 runtime和runtime-with-compiler...
摘要:定義一個組件如下打印如下再回過頭看,可以發現他做的工作就是擴展一個構造函數,并將這個構造函數添加到現在我們已經可以回答最開始的問題的組件是什么的組件其實就是擴展的構造函數,并且在適當的時候實例化為實例。 vue@2.0源碼學習---組件究竟是什么 本篇文章從最簡單的情況入手,不考慮prop和組件間通信。 Vue.component vue文檔告訴我們可以使用Vue.component(...
摘要:觀察員由模板解析指令創建的觀察員負責模板中的更新視圖操作。觀察員種類目前了解情況來看主要分三類視圖指令的計算屬性的用戶自定義的 介紹 關于 Vue.js 的原理一直以來都是一個話題。經過幾天的源碼學習和資料介紹,我將一些個人理解的經驗給寫下來,希望能夠與大家共勉。 附上GITHUB源碼地址, 如果有任何不解 可以在 文章下面提出或者寫下issue, 方便大家回答和學習, 有興趣可以St...
摘要:升級的區別與的斷層式升級不同,延續了自己的風格。在命名方式和上有一些區別,掌握它們是你升級整個項目的關鍵。以下內容都是來源于個人項目的一些經驗之談,并非系統性的闡述。總目錄前端經驗收集器轉載自個人建了前端學習群,旨在一起學習前端。 升級的區別 與angular的斷層式升級不同,vue延續了自己的風格。在命名方式和API上有一些區別,掌握它們是你升級整個項目的關鍵。以下內容都是來源于個人...
摘要:毫無疑問,設計模式于己于他人于系統都是多贏的設計模式使代碼編寫真正工程化設計模小書前端掘金這是一本關于的小書。 JavaScript 常見設計模式解析 - 掘金設計模式(Design pattern)是一套被反復使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。毫無疑問,設計模式于己于他人于系統都是多贏的;設計...
閱讀 662·2021-11-24 09:39
閱讀 2315·2021-11-22 13:54
閱讀 2197·2021-09-23 11:46
閱讀 3246·2019-08-30 15:55
閱讀 2679·2019-08-30 15:54
閱讀 2403·2019-08-30 14:18
閱讀 1546·2019-08-29 14:15
閱讀 2732·2019-08-29 13:49