Vue源碼探究-虛擬節點的實現

Vue源碼探究-虛擬節點的實現

頁面初始化的所有狀態都準備就緒之後,下一步就是要生成組件相應的虛擬節點—— VNode。初次進行組件初始化的時候,VNode 也會執行一次初始化並存儲這時創建好的虛擬節點對象。在隨後的生命週期中,組件內的數據發生變動時,會先生成新的 VNode 對象,然後再根據與之前存儲的舊虛擬節點的對比來執行刷新頁面 DOM 的操作。頁面刷新的流程大致上可以這樣簡單的總結,但是其實現路程是非常複雜的,爲了深入地瞭解虛擬節點生成和更新的過程,首先來看看 VNode 類的具體實現。

VNode 類

VNode 類的實現是支持頁面渲染的基礎,這個類的實現並不複雜,但無論是創建Vue組件實例還是使用動態JS擴展函數組件都運用到了渲染函數 render,它充分利用了 VNode 來構建虛擬DOM樹。

// 定義並導出VNode類
export default class VNode {
  // 定義實例屬性
  tag: string | void; // 標籤名稱
  data: VNodeData | void; // 節點數據
  children: ?Array<VNode>; // 子虛擬節點列表
  text: string | void; // 節點文字
  elm: Node | void; // 對應DOM節點
  ns: string | void; // 節點命名空間,針對svg標籤的屬性
  context: Component | void; // rendered in this component's scope // 組件上下文
  key: string | number | void;  // 節點唯一鍵
  componentOptions: VNodeComponentOptions | void; // 虛擬節點組件配置對象
  componentInstance: Component | void; // component instance // 組件實例
  parent: VNode | void; // component placeholder node // 組件佔位符節點

  // 嚴格內部屬性,有些屬性是服務器渲染的情況使用的,暫時還不瞭解
  // strictly internal
  // 是否包含原始HTML。只有服務器端會使用
  raw: boolean; // contains raw HTML? (server only) 
  // 是否靜態節點,靜態節點將會被提升
  isStatic: boolean; // hoisted static node  
  // 是否在根節點插入,進入轉換檢查所必需的
  isRootInsert: boolean; // necessary for enter transition check
  // 是否空註釋佔位符
  isComment: boolean; // empty comment placeholder?
  // 是否拷貝節點
  isCloned: boolean; // is a cloned node?
  // 是否一次性節點
  isOnce: boolean; // is a v-once node?
  // 異步組件工廠方法
  asyncFactory: Function | void; // async component factory function
  // 異步源
  asyncMeta: Object | void;
  // 是否異步佔位符
  isAsyncPlaceholder: boolean;
  // 服務器端上下文
  ssrContext: Object | void;
  // 功能節點的實際實例上下文
  fnContext: Component | void; // real context vm for functional nodes
  // 方法配置選項,只在服務器渲染使用
  fnOptions: ?ComponentOptions; // for SSR caching
  // 方法作用域id
  fnScopeId: ?string; // functional scope id support

  // 構造函數,參數均可選,與上面定義對應
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    // 實例初始化賦值
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // 定義child屬性的取值器
  // 已棄用:用於向後compat的componentInstance的別名
  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

// 定義並導出createEmptyVNode函數,創建空虛擬節點
export const createEmptyVNode = (text: string = '') => {
  // 實例化虛擬節點
  const node = new VNode()
  // 設置節點文字爲空,並設置爲註釋節點
  node.text = text
  node.isComment = true
  // 返回節點
  return node
}

// 定義並導出createTextVNode函數,創建文字虛擬節點
export function createTextVNode (val: string | number) {
  // 置空實例初始化的標籤名,數據,子節點屬性,只傳入文字
  return new VNode(undefined, undefined, undefined, String(val))
}

// 優化淺拷貝
// 用於靜態節點和插槽節點,因爲它們可以在多個渲染中重用,
// 當DOM操作依賴於它們的elm引用時,克隆它們可以避免錯誤
// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
// 定義並導出cloneVNode函數,拷貝節點
export function cloneVNode (vnode: VNode): VNode {
  // 拷貝節點並返回
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

VNode 類實現的源代碼分兩部分,第一部分是定義 VNode 類自身的實現,第二部分是定一些常用的節點創建方法,包括創建空的虛擬節點,文字虛擬節點和新拷貝節點。虛擬節點本身是一個包含了所有渲染所需信息的載體,從前面一部分的屬性就可以看出,不僅有相應的 DOM 標籤和屬性信息,還包含了子虛擬節點列表,所以一個組件初始化之後得到的 VNode 也是一棵虛擬節點樹,實質是抽象和信息化了的對應於 DOM 樹的 JS 對象。

VNode 的使用在服務器渲染中也有應用,關於這一部分暫時放到之後去研究。

認識到 VNode 的實質之後,對於它的基礎性的作用還是不太清楚,爲什麼需要創建這種對象來呢?答案就在Vue的響應式刷新裏。如前所述,觀察系統實現了對數據變更的監視,在收到變更的通知之後處理權就移交到渲染系統手上,渲染系統首先進行的處理就是根據變動生成新虛擬節點樹,然後再去對比舊的虛擬節點樹,來實現這個抽象對象的更新,簡單的來說就是通過新舊兩個節點樹的對照,來最終確定一個真實DOM建立起來所需要依賴的抽象對象,只要這個真實 DOM 所依賴的對象確定好,渲染函數會把它轉化成真實的 DOM 樹。

最後來概括地描述一下 VNode 渲染成真實 DOM 的路徑:

渲染路徑

Vue 的一般渲染有兩條路徑:

  • 組件實例初始創建生成DOM
  • 組件數據更新刷新DOM

在研究生命週期的時候知道,有 mountupdate 兩個鉤子函數,這兩個生命週期的過程分別代表了兩條渲染路徑的執行。

組件實例初始創建生成DOM

Vue 組件實例初始創建時,走的是 mount 這條路徑,在這條路徑上初始沒有已暫存的舊虛擬節點,要經歷第一輪 VNode 的生成。這一段代碼的執行是從 $mount 函數開始的:

$mount => mountComponent => updateComponent => _render => _update => createPatchFunction(patch) => createElm => insert => removeVnodes

大致描述一下每一個流程中所進行的關於節點的處理:

  • mountComponent 接收了掛載的真實DOM節點,然後賦值給 vm.$el
  • updateComponent 調用 _update,並傳入 _render 生成的新節點
  • _render 生成新虛擬節點樹,它內部是調用實例的 createElement 方法創建虛擬節點
  • _update 方法接收到新的虛擬節點後,會根據是否已有存儲的舊虛擬節點來分離執行路徑,就這一個路徑來說,初始儲存的 VNode 是不存在的,接下來執行 patch 操作會傳入掛載的真實DOM節點和新生成的虛擬節點。
  • createPatchFunction 即是 patch 方法調用的實際函數,執行時會將傳入的真實DOM節點轉換成虛擬節點,然後執行 createElm
  • createElm 會根據新的虛擬節點生成真實DOM節點,內部同樣調用 createElement 方法來創建節點。
  • insert 方法將生成的真實DOM插入到DOM樹中
  • removeVnodes 最後將之前轉換的真實DOM節點從DOM樹中移除

以上就是一般初始化Vue實例組件時渲染的路徑,在這個過程中,初始 VNode 雖然不存在,但是由於掛在的真實 DOM 節點一定存在,所以代碼會按照這樣的流程來執行。

組件數據更新刷新DOM

一般情況下,數據變成會通知 Watcher 實例調用 update 方法,這個方法在一般情況下會把待渲染的數據觀察對象加入到事件任務隊列中,避免開銷過高在一次處理中集中執行。所以在 mount 路徑已經完成了之後,生命週期運行期間都是走的 update 路徑,在每一次的事件處理中 nextTick 會調用 flushSchedulerQueue 來開始一輪頁面刷新:

flushSchedulerQueue => watcher.run => watcher.getAndInvoke => watcher.get => updateComponent => _render => _update => createPatchFunction(patch) => patchVnode => updateChildren

在這個流程中各個方法的大致處理如下:

  • flushSchedulerQueue 調用每一個變更了的數據的監視器的 run 方法
  • run 執行調用實例的 getAndInvoke 方法,目的是獲取新數據並調用監視器的回調函數
  • getAndInvoke 執行的第一步是要獲取變更後的新數據,在這時會調用取值器函數
  • get 執行的取值器函數getter被設定爲 updateComponent,所以會執行繼續執行它
  • updateComponent => createPatchFunction 之間的流程與另一條路徑相同,只是其中基於新舊虛擬節點的判斷不一樣,如果存在舊虛擬節點就執行 patchVnode 操作。
  • patchVnode 方法是實際更新節點的實現,在這個函數的執行中,會得到最終的真實DOM

生命週期中的渲染主要是以上兩條路徑,調用的入口不同,但中間有一部分邏輯是公用的,再根據判斷來選擇分離的路程來更新 VNode 和刷新節點。在這個過程可以看出 VNode 的重要作用。

雖然路徑大致可以這樣總結,但其中的實現比較複雜。不僅在流程判斷上非常有跳躍性,實現更新真實節點樹的操作也都是複雜遞歸的調用。


總的來說虛擬節點的實現是非常平易近人,但是在節點渲染的過程中卻被運用的十分複雜,段位不夠高看了很多遍測試了很多遍才弄清楚整個執行流,這之外還有關於服務器端渲染和持久活躍組件的部分暫時都忽略了。不過關於節點渲染這一部分的實現邏輯非常值得去好好研究。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章