Vue原理解析之Virtual Dom

DOM是文檔對象模型(Document Object Model)的簡寫,在瀏覽器中咱們能夠經過js來操做DOM,可是這樣的操做性能不好,因而Virtual Dom應運而生。個人理解,Virtual Dom就是在js中模擬DOM對象樹來優化DOM操做的一種技術或思路。html

本文將對於Vue框架2.1.8版本中使用的Virtual Dom進行分析。vue

VNode對象

一個VNode的實例對象包含了如下屬性node

  • tag: 當前節點的標籤名web

  • data: 當前節點的數據對象,具體包含哪些字段能夠參考vue源碼types/vnode.d.ts中對VNodeData的定義
    clipboard.png算法

  • children: 數組類型,包含了當前節點的子節點數組

  • text: 當前節點的文本,通常文本節點或註釋節點會有該屬性瀏覽器

  • elm: 當前虛擬節點對應的真實的dom節點app

  • ns: 節點的namespace框架

  • context: 編譯做用域dom

  • functionalContext: 函數化組件的做用域

  • key: 節點的key屬性,用於做爲節點的標識,有利於patch的優化

  • componentOptions: 建立組件實例時會用到的選項信息

  • child: 當前節點對應的組件實例

  • parent: 組件的佔位節點

  • raw: raw html

  • isStatic: 靜態節點的標識

  • isRootInsert: 是否做爲根節點插入,被<transition>包裹的節點,該屬性的值爲false

  • isComment: 當前節點是不是註釋節點

  • isCloned: 當前節點是否爲克隆節點

  • isOnce: 當前節點是否有v-once指令

VNode分類

clipboard.png

VNode能夠理解爲vue框架的虛擬dom的基類,經過new實例化的VNode大體能夠分爲幾類

  • EmptyVNode: 沒有內容的註釋節點

  • TextVNode: 文本節點

  • ElementVNode: 普通元素節點

  • ComponentVNode: 組件節點

  • CloneVNode: 克隆節點,能夠是以上任意類型的節點,惟一的區別在於isCloned屬性爲true

  • ...

createElement解析

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
  // 兼容不傳data的狀況
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 若是alwaysNormalize是true
  // 那麼normalizationType應該設置爲常量ALWAYS_NORMALIZE的值
  if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
  // 調用_createElement建立虛擬節點
  return _createElement(context, tag, data, children, normalizationType)
}

function _createElement (context, tag, data, children, normalizationType) {
  /**
   * 若是存在data.__ob__,說明data是被Observer觀察的數據
   * 不能用做虛擬節點的data
   * 須要拋出警告,並返回一個空節點
   * 
   * 被監控的data不能被用做vnode渲染的數據的緣由是:
   * data在vnode渲染過程當中可能會被改變,這樣會觸發監控,致使不符合預期的操做
   */
  if (data && data.__ob__) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // 當組件的is屬性被設置爲一個falsy的值
  // Vue將不會知道要把這個組件渲染成什麼
  // 因此渲染一個空節點
  if (!tag) {
    return createEmptyVNode()
  }
  // 做用域插槽
  if (Array.isArray(children) &&
      typeof children[0] === 'function') {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 根據normalizationType的值,選擇不一樣的處理方法
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // 若是標籤名是字符串類型
  if (typeof tag === 'string') {
    let Ctor
    // 獲取標籤名的命名空間
    ns = config.getTagNamespace(tag)
    // 判斷是否爲保留標籤
    if (config.isReservedTag(tag)) {
      // 若是是保留標籤,就建立一個這樣的vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
      // 若是不是保留標籤,那麼咱們將嘗試從vm的components上查找是否有這個標籤的定義
    } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 若是找到了這個標籤的定義,就以此建立虛擬組件節點
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 兜底方案,正常建立一個vnode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
    // 當tag不是字符串的時候,咱們認爲tag是組件的構造類
    // 因此直接建立
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  // 若是有vnode
  if (vnode) {
    // 若是有namespace,就應用下namespace,而後返回vnode
    if (ns) applyNS(vnode, ns)
    return vnode
  // 不然,返回一個空節點
  } else {
    return createEmptyVNode()
  }
}

簡單的梳理了一個流程圖,能夠參考下

clipboard.png

patch原理

patch函數的定義在src/core/vdom/patch.js中,咱們先來看下這個函數的邏輯

patch函數接收6個參數:

  • oldVnode: 舊的虛擬節點或舊的真實dom節點

  • vnode: 新的虛擬節點

  • hydrating: 是否要跟真是dom混合

  • removeOnly: 特殊flag,用於<transition-group>組件

  • parentElm: 父節點

  • refElm: 新節點將插入到refElm以前

patch的策略是:

  1. 若是vnode不存在可是oldVnode存在,說明意圖是要銷燬老節點,那麼就調用invokeDestroyHook(oldVnode)來進行銷燬

  2. 若是oldVnode不存在可是vnode存在,說明意圖是要建立新節點,那麼就調用createElm來建立新節點

  3. vnodeoldVnode都存在時

    • 若是oldVnodevnode是同一個節點,就調用patchVnode來進行patch

    • vnodeoldVnode不是同一個節點時,若是oldVnode是真實dom節點或hydrating設置爲true,須要用hydrate函數將虛擬dom和真是dom進行映射,而後將oldVnode設置爲對應的虛擬dom,找到oldVnode.elm的父節點,根據vnode建立一個真實dom節點並插入到該父節點中oldVnode.elm的位置

    這裏面值得一提的是patchVnode函數,由於真正的patch算法是由它來實現的(patchVnode中更新子節點的算法實際上是在updateChildren函數中實現的,爲了便於理解,我統一放到patchVnode中來解釋)。

patchVnode算法是:

  1. 若是oldVnodevnode徹底一致,那麼不須要作任何事情

  2. 若是oldVnodevnode都是靜態節點,且具備相同的key,當vnode是克隆節點或是v-once指令控制的節點時,只須要把oldVnode.elmoldVnode.child都複製到vnode上,也不用再有其餘操做

  3. 不然,若是vnode不是文本節點或註釋節點

    • 若是oldVnodevnode都有子節點,且2方的子節點不徹底一致,就執行更新子節點的操做(這一部分實際上是在updateChildren函數中實現),算法以下

      • 分別獲取oldVnodevnodefirstChildlastChild,賦值給oldStartVnodeoldEndVnodenewStartVnodenewEndVnode

      • 若是oldStartVnodenewStartVnode是同一節點,調用patchVnode進行patch,而後將oldStartVnodenewStartVnode都設置爲下一個子節點,重複上述流程
        clipboard.png

      • 若是oldEndVnodenewEndVnode是同一節點,調用patchVnode進行patch,而後將oldEndVnodenewEndVnode都設置爲上一個子節點,重複上述流程
        clipboard.png

      • 若是oldStartVnodenewEndVnode是同一節點,調用patchVnode進行patch,若是removeOnlyfalse,那麼能夠把oldStartVnode.elm移動到oldEndVnode.elm以後,而後把oldStartVnode設置爲下一個節點,newEndVnode設置爲上一個節點,重複上述流程
        clipboard.png

      • 若是newStartVnodeoldEndVnode是同一節點,調用patchVnode進行patch,若是removeOnlyfalse,那麼能夠把oldEndVnode.elm移動到oldStartVnode.elm以前,而後把newStartVnode設置爲下一個節點,oldEndVnode設置爲上一個節點,重複上述流程
        clipboard.png

      • 若是以上都不匹配,就嘗試在oldChildren中尋找跟newStartVnode具備相同key的節點,若是找不到相同key的節點,說明newStartVnode是一個新節點,就建立一個,而後把newStartVnode設置爲下一個節點

      • 若是上一步找到了跟newStartVnode相同key的節點,那麼經過其餘屬性的比較來判斷這2個節點是不是同一個節點,若是是,就調用patchVnode進行patch,若是removeOnlyfalse,就把newStartVnode.elm插入到oldStartVnode.elm以前,把newStartVnode設置爲下一個節點,重複上述流程
        clipboard.png

      • 若是在oldChildren中沒有尋找到newStartVnode的同一節點,那就建立一個新節點,把newStartVnode設置爲下一個節點,重複上述流程

      • 若是oldStartVnodeoldEndVnode重合了,而且newStartVnodenewEndVnode也重合了,這個循環就結束了

    • 若是隻有oldVnode有子節點,那就把這些節點都刪除

    • 若是隻有vnode有子節點,那就建立這些子節點

    • 若是oldVnodevnode都沒有子節點,可是oldVnode是文本節點或註釋節點,就把vnode.elm的文本設置爲空字符串

  4. 若是vnode是文本節點或註釋節點,可是vnode.text != oldVnode.text時,只須要更新vnode.elm的文本內容就能夠

生命週期

patch提供了5個生命週期鉤子,分別是

  • create: 建立patch時

  • activate: 激活組件時

  • update: 更新節點時

  • remove: 移除節點時

  • destroy: 銷燬節點時

這些鉤子是提供給Vue內部的directives/ref/attrs/style等模塊使用的,方便這些模塊在patch的不一樣階段進行相應的操做,這裏模塊定義在src/core/vdom/modulessrc/platforms/web/runtime/modules2個目錄中

vnode也提供了生命週期鉤子,分別是

  • init: vdom初始化時

  • create: vdom建立時

  • prepatch: patch以前

  • insert: vdom插入後

  • update: vdom更新前

  • postpatch: patch以後

  • remove: vdom移除時

  • destroy: vdom銷燬時

vue組件的生命週期底層其實就依賴於vnode的生命週期,在src/core/vdom/create-component.js中咱們能夠看到,vue爲本身的組件vnode已經寫好了默認的init/prepatch/insert/destroy,而vue組件的mounted/activated就是在insert中觸發的,deactivated就是在destroy中觸發的

實踐

在Vue裏面,Vue.prototype.$createElement對應vdom的createElement方法,Vue.prototype.__patch__對應patch方法,我寫了個簡單的demo來驗證下功能

<p data-height="265" data-theme-id="0" data-slug-hash="rjZKZz" data-default-tab="html,result" data-user="JoeRay" data-embed-version="2" data-pen-title="Vue Virtual Dom" class="codepen">See the Pen Vue Virtual Dom by zhulei (@JoeRay) on CodePen.</p>
<script async src="https://production-assets.cod...

相關文章
相關標籤/搜索