1. 程式人生 > >vue原始碼學習-vnode的掛載和更新流程

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" 字串。

為了便於理解,將流程分為兩個階段:

  1. 首次渲染,生成 vnode,並將其掛載到頁面中
  2. 再次渲染,根據更新後的資料,再次生成 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
}

簡要介紹其屬性:

  1. children 是當前 vnode 的子節點(VNodes)陣列,當前只有一個文字子節點
  2. data 是當前 vnode 代表的節點的各種屬性,是 createElement() 方法的第二個引數
  3. elm 是根據 vnode 生成 HTML 元素掛載到頁面中後對應的 DOM 節點,此時還沒有掛載,所以為空
  4. tag 是當前 vnode 對應的 html 標籤
  5. 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種情況:

  1. 如果 vnode 為 undefined,就要刪除節點
  2. 如果 oldVnode 是 undefined 或者是 DOM 節點,vnode 是 VNode 例項的話,表示是第一次渲染 vnode,呼叫 createElm() 方法建立新節點
  3. 如果 oldVnode 和 vnode 都是 VNode 型別的話,就要呼叫 patchVnode() 方法來對 oldVnode 和 vnode 做進一步處理了,第二階段流程會介紹這種情況

本階段流程是首次渲染,符合第 2 種情況,下面看 createElm() 方法的實現:

createElm(vnode, [], parentElm, refElm) 方法

該方法根據 vnode 的屬性建立元件或者普通 DOM 元素,有如下幾種處理方式:

  1. 呼叫 createComponent() 方法對 component 做處理,這裡就不再展開討論。
  2. vnode.tag 存在:
    1. 呼叫 nodeOps.createElement(tag, vnode) 建立 DOM 元素,
    2. 呼叫 createChildren() 方法遞迴建立子節點。
    3. 呼叫 invokeCreateHooks() 方法呼叫生命週期相關的 create 鉤子處理 vnode.data 資料
  3. 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 還會做一些額外的操作:

  1. 將 vnode 儲存到 vm._vnode 屬性上,供再次渲染檢視時與新 vnode 做比較
  2. 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) 方法

該方法包含兩個主要流程:

  1. 更新自身屬性,呼叫 Vue 內建的元件生命週期 update 階段的鉤子方法更新節點自身的屬性,類似之前的 invokeCreateHooks() 方法,這裡不再展開說明
  2. 更新子節點,根據子節點的不同型別呼叫不同的方法

根據 vnode 的 children 和 text 屬性的取值,子節點有 3 種可能:

  1. children 不為空,text 為空
  2. children 為空,text 不為空
  3. 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 -

可以看到,大概分為這幾類處理方式:

  1. 如果 ch 是 text ,那麼就對 DOM 節點直接設定新的文字;
  2. 如果 ch 為 undefined 了,那麼就清空 DOM 節點的內容
  3. 如果 ch 是 children 型別,而 oldCh是 文字或者為 undefined ,那麼就是在 DOM 節點內新增節點
  4. ch 和 oldCh 都是 children 型別,那麼就要呼叫 updateChildren() 方法來更新 DOM 元素的子節點

updateChildren(elm, oldCh, ch) 方法

updateChildren() 方法是 Vnode 處理方法中最複雜也是最核心的方法,它主要做兩件事情:

  1. 遞迴呼叫 patchVnode 方法處理更下一級子節點
  2. 根據各種判斷條件,對頁面上的 DOM 節點進行儘可能少的新增、移動和刪除操作

下面分析方法的具體實現:

oldCh 和 ch 是代表舊和新兩個 Vnode 節點序列,oldStartIdx、newStartIdx、oldEndIdx、newEndIdx 是 4 個指標,指向 oldCh 和 ch 未處理節點序列中的的開始和結束節點,指向的節點命名為 oldStartVnode、newStartVnode、oldEndVnode、newEndVnode。指標在序列中從兩邊向中間移動,直到 oldCh 或 ch 中的某個序列中的全部節點都處理完畢,這時,如果另一個序列尚有未處理完畢的節點,會再對這些節點進行新增或刪除。

先看 while 迴圈,在 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx 條件下,分為這幾種情況:

  1. isUndef(oldStartVnode) 和 isUndef(oldEndVnode) 在第一次迴圈時是不會觸發的,需要後續條件才可能觸發,下面會分析到
  2. sameVnode(oldStartVnode, newStartVnode) 和 sameVnode(oldEndVnode, newEndVnode) 情況下不用移動 DOM 節點,只移動指標,比如:[A, B] => [A, C]
  3. sameVnode(oldStartVnode, newEndVnode) 情況下,是要將 oldStartVnode 向右移動到 oldEndIdx 對應的節點後面,比如:[A, B] => [C, A]
  4. sameVnode(oldEndVnode, newStartVnode) 情況下,是要將 oldEndVnode 向左移動到 oldStartIdx 對應的節點前面,比如:[A, B] => [B, C]
  5. 在以上條件都不滿足的情況下,就要根據 newStartVnode 的 key 屬性來進一步處理:
    1. 如果 newStartVnode 沒有對應到 oldCh 中的某個元素,比如:[A, B] => [C],說明這個節點是新增加的,那麼就呼叫 createElm() 新建節點及其子節點
    2. 如果 newStartVnode 對應到了 oldCh 中的某個元素,比如:[A, B, C] => [B, A, E],那麼就直接移動該元素到 oldStartIdx 對應的節點前面,同時還會將 oldCh 中對應的節點置為 undefined,表示元素已經處理過了,此時,oldCh == [A, undefined, C],這樣,在後續的迴圈中,就可以觸發 isUndef(oldStartVnode) 或 isUndef(oldEndVnode) 條件了
    3. 另外,還可能會有重複 key 或者 key 相同但是 tag 等屬性不同的情況,比如:[A, B, C] => [B, A, A, C],對於這類情況,newStartVnode 也會被作為新元素處理

迴圈結束時,必然會滿足 oldStartIdx > oldEndIdx 或 newStartIdx > newEndIdx 兩種情況之一,所以對這兩種情況需要進一步處理:

  1. oldStartIdx > oldEndIdx 的情況,比如 [A] => [A, B, C],迴圈結束時,ch 中的 B 和 C 都還沒有新增到頁面中,這時就會呼叫 addVnodes() 方法將他們依次新增
  2. 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 渲染和更新過程中的主要方法和流程,下面是本例中涉及到的主要方法的流程圖:
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 =&gt; created =&gt; beforeMount =&gt; mounted =&gt; 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原始碼學習一--PixAPM的區別

pixhawk是硬體平臺, PX4是pixhawk的原生韌體,專門為pixhawk開發的 APM(Ardupilot Mega)也是硬體 Ardupilot是APM的韌體,所以稱ArduPilot韌體也叫APM 後來APM硬體效能不太夠,所以APM韌體也

Spring原始碼學習之BeanFactoryFactoryBean

今天在學習Spring原始碼的時候,發現了spring中不僅僅有BeanFactory,還有FactoryBean,突然覺得分不清這兩者之間有什麼不同,難道僅僅是名字嗎?但是從名字上我們也能看出一些端