vue原始碼學習-vnode的掛載和更新流程
<div id="app">
{{someVar}}
</div>
<script type="text/javascript">newVue({el:'#app',data:{someVar:'init'},mounted(){setTimeout(() =>this.someVar='changed',3000)
}})
</script>
頁面初始會顯示 "init" 字串,3秒鐘之後,會更新為 "changed" 字串。
為了便於理解,將流程分為兩個階段:
- 首次渲染,生成 vnode,並將其掛載到頁面中
- 再次渲染,根據更新後的資料,再次生成 vnode,並將其更新到頁面中
第一階段
流程
vm.$mount(vm.$el) => render = compileToFunctions(template).render => updateComponent() => vnode = render() => vm._update(vnode) => patch(vm.$el, vnode)
說明
由 render() 方法生成 vnode,然後由 patch() 方法掛載到頁面中。
render() 方法
render() 方法根據當前 vm 的資料生成 vnode。
該方法可以是新建 Vue 例項時傳入的 render() 方法,也可以由 Vue 的 compiler 模組根據傳入的 template 自動生成。
本例中該方法是由 el 屬性對應的 template 生成的,程式碼如下:
(function() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v("\n " + _s(someVar) + "\n ")])
}
})
例項化 Vue 時傳入這樣的引數可以達到相似的效果(區別在於變數兩邊的空格):
new Vue({
data: {
someVar: 'init'
},
render: function(createElement){
return createElement(
'div',
{
attrs: {
"id": "app"
}
},
[
this.someVar
]
)
},
mounted(){
setTimeout(() => this.someVar = 'changed', 3000)
}
}).$mount('#app')
Vnode() 類
Vnode 是虛擬 DOM 節點類,其例項 vnode 是一個包含著渲染 DOM 節點所需要的一切資訊的普通物件。
上述的 render() 方法呼叫後會生成 vnode 物件,這是第一次生成,將其稱為 initVnode,結構如下(選取部分屬性):
{
children: [
{
children: undefined,
data: undefined,
elm: undefined,
tag: undefined,
text: 'init'
}
],
data: {
attrs: {
id: 'app'
}
},
elm: undefined,
tag: 'div',
text: undefined
}
簡要介紹其屬性:
- children 是當前 vnode 的子節點(VNodes)陣列,當前只有一個文字子節點
- data 是當前 vnode 代表的節點的各種屬性,是 createElement() 方法的第二個引數
- elm 是根據 vnode 生成 HTML 元素掛載到頁面中後對應的 DOM 節點,此時還沒有掛載,所以為空
- tag 是當前 vnode 對應的 html 標籤
- text 是當前 vnode 對應的文字或者註釋
children 和 text 是互斥的,不會同時存在。
生成了 vnode 之後,就要根據其屬性生成 DOM 元素並掛載到頁面中了,這是 patch() 方法要做的事情,下面看其內部的流程:
patch(vm.$el, vnode) => createElm(vnode, [], parentElm, nodeOps.nextSibling(oldElm)) => removeVnodes(parentElm, [oldVnode], 0, 0)
patch(oldVnode, vnode) 方法
根據引數的不同,該方法的處理方式也不同,oldVnode 有這幾種可能的取值:undefined、ELEMENT_NODE、VNode,vnode 有這幾種可能的取值:undefined、VNode,所以組合起來一共是 3 * 2 = 6 種處理方式:
oldVnode | vnode | 操作 |
---|---|---|
undefined | undefined | - |
ELEMENT_NODE | undefined | invokeDestroyHook(oldVnode) |
Vnode | undefined | invokeDestroyHook(oldVnode) |
undefined | Vnode | createElm(vnode, [], parentElm, refElm) |
ELEMENT_NODE | Vnode | createElm(vnode, [], parentElm, refElm) |
Vnode | Vnode | patchVnode(oldVnode, vnode) |
可以看到,處理方式可以分為3種情況:
- 如果 vnode 為 undefined,就要刪除節點
- 如果 oldVnode 是 undefined 或者是 DOM 節點,vnode 是 VNode 例項的話,表示是第一次渲染 vnode,呼叫 createElm() 方法建立新節點
- 如果 oldVnode 和 vnode 都是 VNode 型別的話,就要呼叫 patchVnode() 方法來對 oldVnode 和 vnode 做進一步處理了,第二階段流程會介紹這種情況
本階段流程是首次渲染,符合第 2 種情況,下面看 createElm() 方法的實現:
createElm(vnode, [], parentElm, refElm) 方法
該方法根據 vnode 的屬性建立元件或者普通 DOM 元素,有如下幾種處理方式:
- 呼叫 createComponent() 方法對 component 做處理,這裡就不再展開討論。
- vnode.tag 存在:
- 呼叫 nodeOps.createElement(tag, vnode) 建立 DOM 元素,
- 呼叫 createChildren() 方法遞迴建立子節點。
- 呼叫 invokeCreateHooks() 方法呼叫生命週期相關的 create 鉤子處理 vnode.data 資料
- vnode 是文字型別,呼叫 nodeOps.createTextNode(vnode.text) 建立文字元素
對於2,3 這兩種情況,最後都會呼叫 insert() 方法將生成的 DOM 元素掛載到頁面中。此時,頁面的 DOM 結構如下:
<body>
<div id="app">
{{someVar}}
</div>
<div id="app">
init
</div>
</body>
可以看到,原始的 DOM 元素還保留在頁面中,所以在createElm() 方法呼叫之後,還會呼叫 removeVnodes() 方法,將原始的 DOM 元素刪除掉。
這樣,就完成了首次檢視的渲染。在這個過程中,Vue 還會做一些額外的操作:
- 將 vnode 儲存到 vm._vnode 屬性上,供再次渲染檢視時與新 vnode 做比較
- vnode 會更新一些屬性:
{
children: [
{
children: undefined,
data: undefined,
elm: Text, // text
tag: undefined,
text: 'init'
}
],
data: {
attrs: {
id: 'app'
}
},
elm: HTMLDivElement, // div#app
tag: 'div',
text: undefined
}
可以看到,vnode 及其子節點的 elm 屬性更新為了頁面中對應的 DOM 節點,不再是 undefined,也是為了再次渲染時使用。
第二階段
流程
updateComponent() => vnode = render() => vm._update(vnode) => patch(oldVnode, vnode)
第二階段渲染時,會根據更新後的 vm 資料,再次生成 vnode 節點,稱之為 updateVnode,結構如下:
{
children: [
{
children: undefined,
data: undefined,
elm: undefined,
tag: undefined,
text: 'changed'
}
],
data: {
attrs: {
id: 'app'
}
},
elm: undefined,
tag: 'div',
text: undefined
}
可以看到, updateVnode 與 最初生成的 initVnode 的區別就是子節點的 text 屬性由 init 變為了 changed,正是符合我們預期的變化。
生成新的 vnode 之後,還是要呼叫 patch 方法對 vnode 做處理,不過這次引數發生了變化,第一個引數不再是要掛載的DOM節點,而是 initVnode,本次 patch() 方法呼叫的流程如下:
patch(oldVnode, vnode) => patchVnode(oldVnode, vnode) => updateChildren(elm, oldCh, ch) => patchVnode(oldCh, ch) => nodeOps.setTextContent(elm, vnode.text)
其中 oldVnode 就是第一階段儲存的 vm._vnode,elm 就是第一階段更新的 elm 屬性。
根據上面對 patch() 方法的分析,此時 oldVnode 和 vnode 都是 VNode 型別,所以呼叫 patchVnode() 方法做進一步處理。
patchVnode(oldVnode, vnode) 方法
該方法包含兩個主要流程:
- 更新自身屬性,呼叫 Vue 內建的元件生命週期 update 階段的鉤子方法更新節點自身的屬性,類似之前的 invokeCreateHooks() 方法,這裡不再展開說明
- 更新子節點,根據子節點的不同型別呼叫不同的方法
根據 vnode 的 children 和 text 屬性的取值,子節點有 3 種可能:
- children 不為空,text 為空
- children 為空,text 不為空
- children 和 text 都為空
由於 oldVnode 和 vnode 的子節點都有 3 種可能:undefined、children 或 text,所以一共有 3 * 3 = 9 種操作:
oldCh | ch | 操作 |
---|---|---|
children | text | nodeOps.setTextContent(elm, vnode.text) |
text | text | nodeOps.setTextContent(elm, vnode.text) |
undefined | text | nodeOps.setTextContent(elm, vnode.text) |
children | children | updateChildren(elm, oldCh, ch) |
text | children | setTextContent(elm, ''); addVnodes(elm, null, ch, 0, ch.length - 1) |
undefined | children | addVnodes(elm, null, ch, 0, ch.length - 1) |
children | undefined | removeVnodes(elm, oldCh, 0, oldCh.length - 1) |
text | undefined | nodeOps.setTextContent(elm, '') |
undefined | undefined | - |
可以看到,大概分為這幾類處理方式:
- 如果 ch 是 text ,那麼就對 DOM 節點直接設定新的文字;
- 如果 ch 為 undefined 了,那麼就清空 DOM 節點的內容
- 如果 ch 是 children 型別,而 oldCh是 文字或者為 undefined ,那麼就是在 DOM 節點內新增節點
- ch 和 oldCh 都是 children 型別,那麼就要呼叫 updateChildren() 方法來更新 DOM 元素的子節點
updateChildren(elm, oldCh, ch) 方法
updateChildren() 方法是 Vnode 處理方法中最複雜也是最核心的方法,它主要做兩件事情:
- 遞迴呼叫 patchVnode 方法處理更下一級子節點
- 根據各種判斷條件,對頁面上的 DOM 節點進行儘可能少的新增、移動和刪除操作
下面分析方法的具體實現:
oldCh 和 ch 是代表舊和新兩個 Vnode 節點序列,oldStartIdx、newStartIdx、oldEndIdx、newEndIdx 是 4 個指標,指向 oldCh 和 ch 未處理節點序列中的的開始和結束節點,指向的節點命名為 oldStartVnode、newStartVnode、oldEndVnode、newEndVnode。指標在序列中從兩邊向中間移動,直到 oldCh 或 ch 中的某個序列中的全部節點都處理完畢,這時,如果另一個序列尚有未處理完畢的節點,會再對這些節點進行新增或刪除。
先看 while 迴圈,在 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx 條件下,分為這幾種情況:
- isUndef(oldStartVnode) 和 isUndef(oldEndVnode) 在第一次迴圈時是不會觸發的,需要後續條件才可能觸發,下面會分析到
- sameVnode(oldStartVnode, newStartVnode) 和 sameVnode(oldEndVnode, newEndVnode) 情況下不用移動 DOM 節點,只移動指標,比如:[A, B] => [A, C]
- sameVnode(oldStartVnode, newEndVnode) 情況下,是要將 oldStartVnode 向右移動到 oldEndIdx 對應的節點後面,比如:[A, B] => [C, A]
- sameVnode(oldEndVnode, newStartVnode) 情況下,是要將 oldEndVnode 向左移動到 oldStartIdx 對應的節點前面,比如:[A, B] => [B, C]
- 在以上條件都不滿足的情況下,就要根據 newStartVnode 的 key 屬性來進一步處理:
- 如果 newStartVnode 沒有對應到 oldCh 中的某個元素,比如:[A, B] => [C],說明這個節點是新增加的,那麼就呼叫 createElm() 新建節點及其子節點
- 如果 newStartVnode 對應到了 oldCh 中的某個元素,比如:[A, B, C] => [B, A, E],那麼就直接移動該元素到 oldStartIdx 對應的節點前面,同時還會將 oldCh 中對應的節點置為 undefined,表示元素已經處理過了,此時,oldCh == [A, undefined, C],這樣,在後續的迴圈中,就可以觸發 isUndef(oldStartVnode) 或 isUndef(oldEndVnode) 條件了
- 另外,還可能會有重複 key 或者 key 相同但是 tag 等屬性不同的情況,比如:[A, B, C] => [B, A, A, C],對於這類情況,newStartVnode 也會被作為新元素處理
迴圈結束時,必然會滿足 oldStartIdx > oldEndIdx 或 newStartIdx > newEndIdx 兩種情況之一,所以對這兩種情況需要進一步處理:
- oldStartIdx > oldEndIdx 的情況,比如 [A] => [A, B, C],迴圈結束時,ch 中的 B 和 C 都還沒有新增到頁面中,這時就會呼叫 addVnodes() 方法將他們依次新增
- newStartIdx > newEndIdx 的情況,比如 [A, B, C] => [D],迴圈結束時,A, B, C 都還保留在頁面中,這時需要呼叫 removeVnodes() 將他們從頁面中移除
如果迴圈結束時,新舊序列中的節點全部都處理完畢了,如:[A, B] => [B, A],那麼,雖然也會觸發這兩種邏輯之一,但是並不會對 DOM 產生實際的影響。
下面通過一些例子來展示該方法對 DOM 節點的操作流程:
[A, B] => [A, C]
序號 | 說明 | oldStartIdx | oldEndIdx | newStartIdx | newEndIdx | DOM |
---|---|---|---|---|---|---|
0 | 初始狀態 | 0 | 1 | 0 | 1 | A, B |
1 | 第一次迴圈,滿足 sameVnode(oldStartVnode, newStartVnode), 無 DOM 操作 | 1 | 1 | 1 | 1 | A, B |
2 | 第二次迴圈,滿足 isUndef(idxInOld) 條件,新增 C 到 B 之前 | 1 | 1 | 2 | 1 | A, C, B |
2 | 迴圈結束,滿足 newStartIdx > newEndIdx,將 B 移除 | 1 | 1 | 2 | 1 | A, C |
[A, B] => [C, A]
序號 | 說明 | oldStartIdx | oldEndIdx | newStartIdx | newEndIdx | DOM |
---|---|---|---|---|---|---|
0 | 初始狀態 | 0 | 1 | 0 | 1 | A, B |
1 | 第一次迴圈,滿足 sameVnode(oldStartVnode, newEndVnode) ,移動 A 到 B 之後 | 1 | 1 | 0 | 0 | B, A |
2 | 第二次迴圈,滿足 isUndef(idxInOld) 條件,新增 C 到 B 之前 | 1 | 1 | 1 | 0 | C, B, A |
2 | 迴圈結束,滿足 newStartIdx > newEndIdx,將 B 移除 | 1 | 1 | 1 | 0 | C, A |
[A, B, C] => [B, A, E]
序號 | 說明 | oldCh | oldStartIdx | oldEndIdx | ch | newStartIdx | newEndIdx | DOM |
---|---|---|---|---|---|---|---|---|
0 | 初始狀態 | [A, B, C] | 0 | 2 | [B, A, E] | 0 | 2 | A, B, C |
1 | 第一次迴圈,滿足 sameVnode(elmToMove, newStartVnode),移動 B 到 A 之前 | [A, undefined, C] | 0 | 2 | [B, A, E] | 1 | 2 | B, A, C |
2 | 第二次迴圈,滿足 sameVnode(oldStartVnode, newStartVnode),無 DOM 操作 | [A, undefined, C] | 1 | 2 | [B, A, E] | 2 | 2 | B, A, C |
3 | 第三次迴圈,滿足 isUndef(oldStartVnode),無 DOM 操作 | [A, undefined, C] | 2 | 2 | [B, A, E] | 2 | 2 | B, A, C |
4 | 第四次迴圈,滿足 isUndef(idxInOld),新增 E 到 C 之前 | [A, undefined, C] | 2 | 2 | [B, A, E] | 3 | 2 | B, A, E, C |
5 | 迴圈結束,滿足 newStartIdx > newEndIdx,將 C 移除 | [A, undefined, C] | 2 | 2 | [B, A, E] | 3 | 2 | B, A, E |
[A] => [B, A]
序號 | 說明 | oldStartIdx | oldEndIdx | newStartIdx | newEndIdx | DOM |
---|---|---|---|---|---|---|
0 | 初始狀態 | 0 | 0 | 0 | 1 | A |
1 | 第一次迴圈,滿足 sameVnode(oldStartVnode, newEndVnode),無 DOM 操作 | 1 | 0 | 0 | 0 | A |
2 | 迴圈結束,滿足 oldStartIdx > oldEndIdx ,新增 B 到 A 之前 | 1 | 0 | 0 | 1 | B, A |
[A, B] => [B, A]
序號 | 說明 | oldStartIdx | oldEndIdx | newStartIdx | newEndIdx | DOM |
---|---|---|---|---|---|---|
0 | 初始狀態 | 0 | 1 | 0 | 1 | A, B |
1 | 第一次迴圈,滿足 sameVnode(oldStartVnode, newEndVnode),移動 A 到 B 之後 | 1 | 1 | 0 | 0 | B, A |
2 | 第二次迴圈,滿足 sameVnode(oldStartVnode, newStartVnode) 條件,無 DOM 操作 | 2 | 1 | 1 | 0 | B, A |
3 | 迴圈結束,滿足 oldStartIdx > oldEndIdx ,無 DOM 操作 | 2 | 1 | 1 | 0 | B, A |
通過以上流程,檢視再次得到了更新。同時,新的 vnode 和 elm 也會被儲存,供下一次檢視更新時使用。
以上分析了 Vnode 渲染和更新過程中的主要方法和流程,下面是本例中涉及到的主要方法的流程圖:
相關推薦
vue原始碼學習-vnode的掛載和更新流程
<div id="app"> {{someVar}} </div> <script type="text/javascript">newVue({el:'#app',data:{someVar:'init'},
vue 原始碼學習二 例項初始化和掛載過程
vue 入口 從vue的構建過程可以知道,web環境下,入口檔案在 src/platforms/web/entry-runtime-with-compiler.js(以Runtime + Compiler模式構建,vue直接執行在瀏覽器進行編譯工作) import Vue from './runtime/
vue 原始碼學習(二) 例項初始化和掛載過程
vue 入口 從vue的構建過程可以知道,web環境下,入口檔案在 src/platforms/web/entry-runtime-with-compiler.js(以Runtime + Compiler模式構建,vue直接執行在瀏覽器進行編譯工作) import Vue from './runtime/
vue 原始碼學習(一) 目錄結構和構建過程簡介
Flow vue框架使用了Flow作為型別檢查,來保證專案的可讀性和維護性。vue.js的主目錄下有Flow的配置.flowconfig檔案,還有flow目錄,指定了各種自定義型別。 在學習原始碼前可以先看下Flow的語法 官方文件 目錄結構 vue.js原始碼主要在src下 src ├── com
LitePal原始碼學習(2)——更新表流程(更新資料庫)
add 2018/6/14上一篇我講述了LitePal建立表(建立資料庫的流程),連結戳這裡。這一篇看看LitePal是如何做到簡便的升級資料庫的。加入你兩張表Singer和Musiic,並且已經存了資料,結果發現Music表名字多打了一個 i ,Singer多了一個欄位,並
Vue.js 原始碼學習五 —— provide 和 inject 學習
早上好!繼續開始學習Vue原始碼吧~ 在 Vue.js 的 2.2.0+ 版本中新增加了 provide 和 inject 選項。他們成對出現,用於父級元件向下傳遞資料。 下面我們來看看原始碼~ 原始碼位置 和之前一樣,初始化的方法都是在 V
vue原始碼學習——觀察者模式
情景:接觸過vue的同學都知道,我們曾經都很好奇為什麼vue能這麼方便的進行資料處理,當一個物件的某個狀態改變之後,只要依賴這個資料顯示的部分也會發生改變,如果你依舊很好奇,那麼今天你就可以瞭解一下實現的原理 什麼是觀察者模式 官方解釋是
vue原始碼學習——資料雙向繫結的Object.defineProperty
情景:vue雙向繫結,這應該是多數講vue優勢脫口而出的名詞,然後你就會接觸到一個方法 Object.defineProperty(a,"b",{}) 這個方法該怎麼用 簡單例子敲一下 var a = {} Object.defineProperty(a,"b
Vue原始碼學習(二)——生命週期
官網對生命週期給出了一個比較完成的流程圖,如下所示: 從圖中我們可以看到我們的Vue建立的過程要經過以下的鉤子函式: beforeCreate => created => beforeMount => mounted => beforeUpda
vue原始碼解讀-dom掛載1
vue框架是一款雙向資料繫結的框架,能大大提高我們的開發效率。那麼vue是如何將模板生成dom的呢,通過閱讀原始碼可以知道這一過程。 <!DOCTYPE html> <html lang="en"> <head> &l
Vue原始碼學習之——如何在Chrome中deBug原始碼
參考連結 如果我們不用單檔案元件開發,一般直接<script src="dist/vue.js">引入開發版vue.js 這種情況下debug也是很方便的,只不過vue.js檔案程式碼是rollup生成的 但是如果能夠在vue專案中的src目錄下中的檔案打斷點除錯就
Vue原始碼學習(4)——資料響應系統
Vue原始碼學習(4)——資料響應系統:通過initData() 看資料響應系統 下面,根據理解我們寫一個簡略的原始碼:參考 治癒watcher在:vm.$mount(vm.$options.el) Function de
vue原始碼學習(2)——建構函式
vue原始碼學習——建構函式 下圖從上到下是從package.json沿路找到建構函式的路徑;從下到上是建構函式的構造路徑,檢視建構函式添加了什麼方法。 PS: 以上的流程掛載了很多方法,但是注意:這個時候方法並沒有被呼
Vue原始碼學習(1)——目錄結構
Vue原始碼學習——目錄結構 參考博主 目錄結構: -scripts:包含與構建相關的指令碼和配置檔案。 -dist:構建後文件的輸出目錄 -flow:包含Flow的型別宣告。這些宣告是全域性載入的,將在普通原始碼中看到它們在型別註釋中使用。 -packages:
Vue原始碼學習二 ———— Vue原型物件包裝
Vue原型物件的包裝 在Vue官網直接通過 script 標籤匯入的 Vue包是 umd模組的形式。在使用前都通過 new Vue({})。記錄一下 Vue建構函式的包裝。 在 src/core/instance/index.js 這個檔案是 Vue建構函式的出生地。 import { initMixi
Vue原始碼學習三 ———— Vue建構函式包裝
Vue原始碼學習二 是對Vue的原型物件的包裝,最後從Vue的出生檔案匯出了 Vue這個建構函式 來到 src/core/index.js 程式碼是: import Vue from './instance/index' import { initGlobalAPI } from './g
vue原始碼學習——虛擬dom樹是如何定義的
情景:相信通過前面的學習你已經知道了虛擬dom為什麼會被構思,那麼接下來你好奇的應該是作者該如何定義這個虛擬dom export default class VNode { tag: string | void;//當前節點的標籤名 data: VNodeData
一步一腳印的 iOS App 上架和更新流程
彼得潘的 Swift 程式設計入門,App程式設計入門作者,彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,http://apppeterpan.strikingly.com
PX4原始碼學習一--Pix和APM的區別
pixhawk是硬體平臺, PX4是pixhawk的原生韌體,專門為pixhawk開發的 APM(Ardupilot Mega)也是硬體 Ardupilot是APM的韌體,所以稱ArduPilot韌體也叫APM 後來APM硬體效能不太夠,所以APM韌體也
Spring原始碼學習之BeanFactory和FactoryBean
今天在學習Spring原始碼的時候,發現了spring中不僅僅有BeanFactory,還有FactoryBean,突然覺得分不清這兩者之間有什麼不同,難道僅僅是名字嗎?但是從名字上我們也能看出一些端