虛擬 DOM 與 Diff 算法的實現原理

前言

Vue 源碼中虛擬 DOM 與 Diff 算法的實現借鑑了 snabbdom 這個庫,snabbdom 是一個虛擬 DOM 庫,它專一於簡單,模塊化,強大的功能和性能。要完全明白虛擬 DOM 與 Diff 算法就得分析 snabbdom 這個庫到底作了什麼?javascript

獲取源代碼

能夠經過npm i snabbdom -D 來下載 snabbdom 庫,這樣咱們既能看到 src 下用 Typescript 編寫的源碼,也能看到編譯好的 JavaScript 代碼。下面貼的源碼是 2.1.0 版本,如今已經更新到 3.0.3 版本了。建議將下方出現的源碼複製到下載的 snabbdom 庫中相應位置,這樣看源碼比較清晰。那咱們就開始分析源碼吧。java

源碼分析

JavaScript 對象模擬真實 DOM 樹

經過調用 snabbdom 庫中的 h函數就能夠對真實 DOM 節點進行抽象。咱們先來看看一個完整的虛擬 DOM 節點(vnode)是什麼樣的:node

{
  sel: "div", // 當前vnode的選擇器
  elm: undefined, // 當前vnode對應真實的DOM節點
  key: undefined, // 當前vnode的惟一標識
  data: {}, // 當前vnode的屬性,樣式等
  children: undefined, // 當前vnode的子元素
  text: '文本內容' // 當前vnode的文本節點內容
}

實際上,h函數的做用就是用 JavaScript 對象模擬真實的 DOM 樹,對真實 DOM 樹進行抽象。調用 h函數就能獲得由 vnode 組成的虛擬 DOM 樹。
image.png
調用 h函數有多種形式:算法

① h('div')
 ② h('div', 'text')
 ③ h('div', h('p'))
 ④ h('div', [])
 ⑤ h('div', {})
 ⑥ h('div', {}, 'text')
 ⑦ h('div', {}, h('span'))
 ⑧ h('div', {}, [])

使得 h函數的第二和第三個參數比較靈活,要判斷的狀況也比較多,下面把這部分的核心源碼分析貼一下:npm

// h函數:根據傳入的參數推測出h函數的調用形式以及每一個vnode對應屬性的屬性值
export function h(sel: string): VNode
export function h(sel: string, data: VNodeData | null): VNode
export function h(sel: string, children: VNodeChildren): VNode
export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h(sel: any, b?: any, c?: any): VNode {
  var data: VNodeData = {};
  var children: any;
  var text: any;
  var i: number
  // c有值,狀況有:⑥ ⑦ ⑧
  if (c !== undefined) { 
    // c有值的狀況下b有值,狀況有:⑥ ⑦ ⑧
    if (b !== null) { 
      // 將b賦值給data 
      data = b  
    }
    // c的數據類型是數組,狀況有:⑧
    if (is.array(c)) { 
      children = c 
    // 判斷c是文本節點,狀況有:⑥
    } else if (is.primitive(c)) { 
      text = c 
    // 狀況有:⑦,⑦這條語句會先執行h('span')代碼,直接調用vnode函數,調用後會返回{sel: 'span'},
    // 這時c有值而且c而且含有sel屬性
    } else if (c && c.sel) {
      // 注:這裏的c不是h('span'),而是h('span')的返回值,是個{ sel: 'span' }這樣的對象,
      // 最後組裝成數組賦值給children
      children = [c]
    }
  // c沒有值,b有值,狀況有:② ③ ④ ⑤
  } else if (b !== undefined && b !== null) { 
    // b的數據類型是數組,狀況有:④
    if (is.array(b)) { 
      children = b 
    // 判斷b是文本節點,狀況有:②
    } else if (is.primitive(b)) { 
      text = b 
    // 狀況有:③,③這條語句會先執行h('p')代碼,直接調用vnode函數,調用後會返回{sel: 'p'},
    // 這時b有值而且b而且含有sel屬性
    } else if (b && b.sel) {
      // 注:這裏的b不是h('p'),而是h('p')的返回值,是個{ sel: 'p' }這樣的對象,
      // 最後組裝成數組賦值給children
      children = [b] 
    // 狀況有:⑤,將b賦值給data
    } else { data = b } 
  }
  // children有值,遍歷children
  if (children !== undefined) { 
    for (i = 0; i < children.length; ++i) {
      // 判斷children中的每一項的數據類型是不是string/number,調用vnode函數
      if (is.primitive(children[i])) {
          children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
      }
    }
  }
  /**
   * 調用vnode後返回形如
   * {
   *    sel: 'div',
   *    data: { style: '#000' },
   *    children: undefined,
   *    text: 'text',
   *    elm: undefined, 
   *    key: undefined
   * }
   * 這樣的JavaScript對象
  */
  return vnode(sel, data, children, text, undefined);  
}
// vnode函數:根據傳入的參數組裝vnode結構
export function vnode(sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  // 判斷data是否有值,有值就將data.key賦值給key,無值就將undefined賦值給key
  const key = data === undefined ? undefined : data.key 
  // 將傳入vnode函數的參數組裝成一個對象返回
  return { sel, data, children, text, elm, key } 
}

diff 算法--入口函數

經過 h函數獲得新舊虛擬節點 DOM 對象後就能夠進行差別比較了。在實際使用過程當中,咱們是直接調用 snabbdompatch 函數,而後傳入兩個參數,經過 patch 函數內部處理就能夠獲得新舊虛擬節點 DOM 對象的差別,並將差別部分更新到真正的 DOM 樹上。api

首先,patch 函數會去判斷 oldVnode 是不是真實DOM節點,若是是則須要先轉換爲虛擬DOM節點 oldVnode = emptyNodeAt(oldVnode) ;而後去比較新舊 vnode 是不是同一個節點 sameVnode(oldVnode, vnode),若是是同一節點則精確比較新舊 vnode patchVnode(oldVnode, vnode, insertedVnodeQueue) ,若是不是則直接建立新 vnode 對應的真實 DOM 節點 createElm(vnode, insertedVnodeQueue),在 createElm 函數中建立新 vnode 的真實 DOM 節點以及它對應的子節點,並把子節點插入到相應位置。若是 oldVnode.elm 有父節點則把新 vnode 對應的真實 DOM 節點做爲子節點插入到相應位置,而且刪除舊節點。下面貼一下 patch 函數的源碼解析:數組

function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

    // isVnode(oldVnode)判斷oldVnode.sel是否存在,不存在表示oldVnode是真實的DOM節點
    if (!isVnode(oldVnode)) {
      // oldVnode多是真實的DOM節點,也多是舊的虛擬DOM節點,
      // 若是是真實的DOM節點要調用vnode函數組裝成虛擬DOM節點
      oldVnode = emptyNodeAt(oldVnode)
    }

    // 判斷出是同一個虛擬DOM節點
    if (sameVnode(oldVnode, vnode)) { 
      // 精確比較兩個虛擬DOM節點
      patchVnode(oldVnode, vnode, insertedVnodeQueue) 
    } else {
      // oldVnode.elm是虛擬DOM節點對應的真實DOM節點
      elm = oldVnode.elm! 
      // api.parentNode(elm)獲取elm的父節點elm.parentNode
      parent = api.parentNode(elm) as Node 

      // 建立vnode下真實DOM節點並更新到相應位置
      createElm(vnode, insertedVnodeQueue) 

      // elm的父節點存在
      if (parent !== null) { 
        // api.nextSibling(elm)-->elm.nextSibling 返回緊跟在elm以後的節點
        // api.insertBefore(parent, B, C)-->-->parent.insertBefore(B, C),將B節點插入到C節點以前
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        removeVnodes(parent, [oldVnode], 0, 0) // 刪除舊的DOM節點
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
  }

patch 函數中用到了 emptyNodeAt 函數,這個函數主要是處理 patch 函數第一個參數爲真實DOM節點的狀況。下面貼一下這個函數的源碼解析:app

function emptyNodeAt(elm: Element) {
    // 判斷傳入的DOM節點elm有沒有id屬性,由於虛擬DOM節點的sel屬性是選擇器,例如:div#wrap
    const id = elm.id ? '#' + elm.id : '' 
    // 判斷傳入的ODM節點elm有沒有class屬性,由於虛擬DOM節點的sel屬性是選擇器,例如:div.wrap
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '' 
    // 調用vnode函數將傳入的DOM節點組裝成虛擬DOM節點
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm) 
  }

patch 函數中用到了 sameVnode 函數,這個函數主要用來比較兩個虛擬DOM節點是不是同一個虛擬節點。下面貼一下這個函數的源碼分析:dom

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  // 判斷vnode1和vnode2是不是同一個虛擬DOM節點
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel 
}

diff 算法--新舊 vnode 不是同一個節點的狀況

根據 sameVnode 函數返回的結果,新舊 vnode 不是同一個虛擬節點。首先獲取到 oldVnode 對應真實 DOM 節點的父節點 parent ,而後調用 createElm 函數去建立 vnode 對應的真實 DOM 節點以及它的子節點和標籤屬性等等。判斷是否有 parent, 若是有則將 vnode.elm 對應的DOM節點做爲子節點插入到 parent 節點下的相應位置。部分源碼分析在patch函數中,下面貼一下 createElm 函數的源碼分析:模塊化

function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any
    let data = vnode.data
    if (data !== undefined) {
      const init = data.hook?.init
      if (isDef(init)) {
        init(vnode)
        data = vnode.data
      }
    }
    const children = vnode.children
    const sel = vnode.sel
    // 判斷sel值中是否包含!
    if (sel === '!') {
      if (isUndef(vnode.text)) {
        vnode.text = ''
      }
      // --> document.createComment(vnode.text!)建立註釋節點
      vnode.elm = api.createComment(vnode.text!)
    } else if (sel !== undefined) {
      // 解析sel選擇器
      // 查找sel屬性值中#的索引,沒找到返回-1
      const hashIdx = sel.indexOf('#')
      // hashIdx做爲起始位置查找sel屬性值中.的索引,若是hashIdx < 0 那麼從位置0開始查找
      const dotIdx = sel.indexOf('.', hashIdx)
      const hash = hashIdx > 0 ? hashIdx : sel.length
      const dot = dotIdx > 0 ? dotIdx : sel.length
      // 若id選擇器或class選擇器存在,則從0位開始截取到最小索引值的位置結束,截取出的就是標籤名稱
      // 都不存在直接取sel值
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
      // 根據tag名建立DOM元素
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag)
      // 設置id屬性
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
      // 設置calss屬性
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
      // 判斷children是不是數組,是數組則遍歷children
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i]
          if (ch != null) {
            // createElm(ch as VNode, insertedVnodeQueue)遞歸建立子節點
            // api.appendChild(A, B)-->A.appendChild(B)將B節點插入到指定父節點A的子節點列表的末尾
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
          }
        }
        // 判斷vnode.text有沒有值
      } else if (is.primitive(vnode.text)) {
        // api.createTextNode(vnode.text)根據vnode.text建立文本節點
        // api.appendChild(elm, B)-->A.appendChild(B)將文本節點B添加到父節點elm子節點列表的末尾處
        api.appendChild(elm, api.createTextNode(vnode.text))
      }
      const hook = vnode.data!.hook
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode)
        if (hook.insert) {
          insertedVnodeQueue.push(vnode)
        }
      }
    } else {
      // sel不存在直接建立文本節點
      vnode.elm = api.createTextNode(vnode.text!)
    }
    return vnode.elm
  }

diff 算法--新舊 vnode 是同一個節點的狀況

上面分析了新舊 vnode 不是同一個虛擬節點,那麼是同一個虛擬節點又該怎麼去處理?首先,調用 patchVnode 函數 patchVnode(oldVnode, vnode, insertedVnodeQueue),這個函數會對新舊 vnode 進行精確比較:

① 若是新舊虛擬 DOM 對象全等 oldVnode === vnode ,那麼不作任何操做,直接返回;

② 而後判斷 vnode 是否有文本節點 isUndef(vnode.text) ,若是沒有文本節點則判斷 oldVnode 與 vnode 有沒有子節點 isDef(oldCh) && isDef(ch),若是都有子節點且不相等則調用 updateChildren 函數去更新子節點;

③ 若是隻有 vnode 有子節點而 oldVnode 有文本節點或沒有內容,將 oldVnode 的文本節點置空或不作處理,調用 addVnodes 函數將 vnode 的子節點建立出對應的真實DOM並循環插入到父節點下;

④ 若是隻有 oldVnode 有子節點而 vnode 沒有內容,則直接刪除 oldVnode 下的子節點;

⑤ 若是隻有 oldVnode 有文本節點而 vnode 沒有內容,則將 oldVnode 對應的真實 DOM 節點的文本置空;

⑥ 若是 vnode 有文本節點,oldVnode 有子節點就將對應真實 DOM 節點的子節點刪除,沒有就不處理,而後將 vnode 的文本節點做爲子節點插入到對應真實 DOM 節點下。

部分源碼分析在patch函數中,下面貼一下 patchVnode 函數的源碼分析:

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    const hook = vnode.data?.hook
    hook?.prepatch?.(oldVnode, vnode)
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    // oldVnode與vnode徹底相等並無須要更新的內容則直接返回,不作處理
    if (oldVnode === vnode) return 
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      vnode.data.hook?.update?.(oldVnode, vnode)
    }
    // vnode.text爲undefined表示vnode虛擬節點沒有文本內容
    if (isUndef(vnode.text)) { 
      // oldCh與ch都不爲undefined表示oldVnode與vnode都有虛擬子節點children
      if (isDef(oldCh) && isDef(ch)) { 
        // oldCh !== ch 利用算法去更新子節點
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      } else if (isDef(ch)) { 
        // 將oldVnode的文本節點設置爲''
        if (isDef(oldVnode.text)) api.setTextContent(elm, '') 
        // 調用addVnodes方法將vnode的虛擬子節點循環插入到elm節點的子列表下
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      // oldCh不爲undefined表示oldVnode有虛擬子節點children
      } else if (isDef(oldCh)) { 
        // vnode沒有children則直接刪除oldVnode的children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1) 
      // oldVnode.text有值而vnode.text沒有值
      } else if (isDef(oldVnode.text)) { 
        // 將oldVnode的文本節點設置爲''
        api.setTextContent(elm, '') 
      }
    // oldVnode與vnode文本節點內容不一樣
    } else if (oldVnode.text !== vnode.text) { 
      // isDef(oldCh)-->oldCh !== undefined 代表oldVnode虛擬節點下有虛擬子節點
      if (isDef(oldCh)) { 
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      }
      // oldCh虛擬節點下沒有虛擬子節點則直接更新文本內容
      api.setTextContent(elm, vnode.text!)
    }
    hook?.postpatch?.(oldVnode, vnode)
  }

diff 算法--新舊 vnode 子節點的更新策略

當新舊 vnode 都有子節點時,diff 算法定義了四個指針來處理子節點,四個指針分別是:oldStartVnode(舊前vnode)/newStartVnode(新前vnode)/oldEndVnode(舊後vnode)/newEndVnode(新後vnode) 。進入循環體內後,新舊 vnode 的子節點兩兩比較,這裏提供了一套比較規則,以下圖:
image.png
若是上面四個規則都不知足,將 oldVnode 的子節點從舊的前索引 oldStartIdx 到舊的後索引 oldEndIdx 作一個 key 與對應位置序號的映射 oldKeyToIdx ,經過新 vnode 的 key 去找 oldKeyToIdx 中是否有對應的索引值,若沒有,代表 oldVnode 沒有對應的舊節點,是一個新增的節點,進行插入操做;如有,代表 oldVnode 有對應的舊節點,不是一個新增節點,進行移動操做。下面貼一下源碼解析:

// 舊vnode的子節點的前索引oldStartIdx到後索引oldEndIdx的key與對應位置序號的映射關係
function createKeyToOldIdx(children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
  const map: KeyToIndexMap = {}
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key
    if (key !== undefined) {
      map[key] = i
    }
  }
  /**
   * 例如:map = { A: 1, B: 2 }
  */
  return map
}
function updateChildren(parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0 // 舊的前索引
    let newStartIdx = 0 // 新的前索引
    let oldEndIdx = oldCh.length - 1 // 舊的後索引
    let newEndIdx = newCh.length - 1 // 新的後索引
    let oldStartVnode = oldCh[0] // 舊的前vnode
    let newStartVnode = newCh[0] // 新的前vnode
    let oldEndVnode = oldCh[oldEndIdx] // 舊的後vnode
    let newEndVnode = newCh[newEndIdx] // 新的後vnode
    let oldKeyToIdx: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any

    // 當舊的前索引 <= 舊的後索引 && 新的前索引 <= 新的後索引時執行循環語句
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 爲何oldStartVnode == null? 
      // 由於虛擬節點進行移動操做後要將原來的虛擬節點置爲undefined了
      // oldCh[idxInOld] = undefined as any
      if (oldStartVnode == null) {
        // oldStartVnode爲null就過濾掉當前節點,取oldCh[++oldStartIdx]節點(舊的前索引的下一個索引的節點)
        oldStartVnode = oldCh[++oldStartIdx]
      } else if (oldEndVnode == null) {
        // oldEndVnode爲null就過濾掉當前節點,取oldCh[--oldEndIdx]節點(舊的後索引的上一個索引的節點)
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (newStartVnode == null) {
        // newStartVnode爲null就過濾掉當前節點,取newCh[++newStartIdx]節點(新的前索引的下一個索引的節點)
        newStartVnode = newCh[++newStartIdx]
      } else if (newEndVnode == null) {
        // newEndVnode爲null就過濾掉當前節點,取newCh[--newEndIdx]節點(新的後索引的上一個索引的節點)
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        /**
        * ① 舊的前vnode(oldStartVnode) 與 新的前vnode(newStartVnode) 比較是不是同一個虛擬節點
        * 舊的虛擬子節點                       新的虛擬子節點
        * h('li', { key: 'A' }, 'A')      h('li', { key: 'A' }, 'A')
        * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')
       */
        // 若是判斷是同一個虛擬節點則調用patchVnode函數
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        // oldCh[++oldStartIdx]取舊的前索引節點的下一個虛擬節點(例子中key爲B的節點),賦值給oldStartVnode
        oldStartVnode = oldCh[++oldStartIdx]
        // oldCh[++oldStartIdx]取新的前索引節點的下一個虛擬節點(例子中key爲B的節點),賦值給newStartVnode
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        /**
         * 若是舊的前vnode(例子中key爲B的虛擬節點) 與 新的前vnode(例子中key爲B的虛擬節點) 
         * 不是同一個虛擬節點則進行方案②比較
         * ② 舊的後vnode(oldEndVnode) 與 新的後vnode(newEndVnode) 比較是不是同一個虛擬節點
         * 舊的虛擬子節點                   新的虛擬子節點
         * h('li', { key: 'C' }, 'C')      h('li', { key: 'A' }, 'A')
         * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')
        */
        // 若是判斷是同一個虛擬節點則調用patchVnode函數
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        // oldCh[--oldEndIdx]取舊的後索引節點的上一個虛擬節點(例子中key爲C的虛擬節點),賦值給oldEndVnode
        oldEndVnode = oldCh[--oldEndIdx]
        // newCh[--newEndIdx]取新的後索引節點的上一個虛擬節點(例子中key爲A的虛擬節點),賦值給newEndVnode
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        /**
        * 若是舊的後vnode 與 新的後vnode 不是同一個虛擬節點則進行方案③比較
        * ③ 舊的前vnode(oldStartVnode) 與 新的後vnode(newEndVnode) 比較是不是同一個虛擬節點
        * 舊的虛擬子節點                   新的虛擬子節點
        * h('li', { key: 'C' }, 'C')      h('li', { key: 'A' }, 'A')
        * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')
        *                                 h('li', { key: 'C' }, 'C')
       */
        // 若是判斷是同一個虛擬節點則調用patchVnode函數
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 將舊的前vnode(至關於例子中key爲C的虛擬節點)插入到當前舊的後vnode的下一個兄弟節點的前面
        // 若是oldEndVnode是最末尾的虛擬節點,則node.nextSibling會返回null,
        // 則新的虛擬節點直接插入到最末尾,等同於appenChild
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
        // oldCh[++oldStartIdx]取舊的前索引虛擬節點的下一個虛擬節點(例子中key爲B的虛擬節點),賦值給oldStartVnode
        oldStartVnode = oldCh[++oldStartIdx]
        // newCh[--newEndIdx]取新的後索引虛擬節點的上一個虛擬節點(例子中key爲B的虛擬節點),賦值給newEndVnode
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        /**
        * 若是舊的前vnode 與 新的後vnode 不是同一個虛擬節點則進行方案④比較
        * ④ 舊的後vnode(oldEndVnode) 與 新的前vnode(newStartVnode) 比較是不是同一個虛擬節點
        * 舊的虛擬子節點                   新的虛擬子節點
        * h('li', { key: 'C' }, 'C')      h('li', { key: 'B' }, 'B')
        * h('li', { key: 'B' }, 'B')      h('li', { key: 'A' }, 'A')
        *                                 h('li', { key: 'C' }, 'C')
       */
        // 若是判斷是同一個虛擬節點則調用patchVnode函數
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 將舊的後vnode(例子中key爲B)插入到當前舊的前vnode(例子中key爲C)的前面
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
        // oldCh[--oldEndIdx]取舊的後索引節點的上一個虛擬節點(例子中key爲C的虛擬節點),賦值給oldEndVnode
        oldEndVnode = oldCh[--oldEndIdx]
        // newCh[++newStartIdx]取新的前索引節點的下一個虛擬節點(例子中key爲A的虛擬節點),賦值給newStartVnode
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 不知足以上四種狀況
        if (oldKeyToIdx === undefined) {
          // oldKeyToIdx保存舊的children中各個節點的key與對應位置序號的映射關係
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }
        // 從oldKeyToIdx中獲取當前newStartVnode節點key對應的序號
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        if (isUndef(idxInOld)) { // isUndef(idxInOld) --> idxInOld === undefined
          /**
           * idxInOld = undefined 要插入節點
           * 舊的虛擬子節點中沒有idxInOld對應的節點,而新的虛擬子節點中有,
           * 因此newStartVnode是須要插入的虛擬節點
           * 舊的虛擬子節點                   新的虛擬子節點
           * h('li', { key: 'A' }, 'A')      h('li', { key: 'C' }, 'C') 
           * h('li', { key: 'B' }, 'B')
          */
          // 根據newStartVnode(例子中key爲C的虛擬節點)建立真實DOM節點createElm(),
          // 將建立的DOM節點插入到oldStartVnode.elm(例子中key爲A的節點)的前面
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          /**
           * idxInOld != undefined 要移動節點
           * 舊的虛擬子節點中有idxInOld對應的節點,因此oldCh[idxInOld]是須要移動的虛擬節點
           * 舊的虛擬子節點                   新的虛擬子節點
           * h('li', { key: 'A' }, 'A')      h('div', { key: 'B' }, 'B')
           * h('li', { key: 'B' }, 'B')      h('li', { key: 'D' }, 'D')                                                      
          */
          elmToMove = oldCh[idxInOld] // elmToMove保存要移動的虛擬節點
          // 判斷elmToMove與newStartVnode在key相同的狀況下sel屬性是否相同
          if (elmToMove.sel !== newStartVnode.sel) {
            // sel屬性不相同代表不是同一個虛擬節點,
            // 根據newStartVnode虛擬節點建立真實DOM節點並插入到oldStartVnode.elm(舊的key爲A的節點)以前
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
            // key與sel相同表示是同一個虛擬節點,調用patchVnode函數
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            // 處理完被移動的虛擬節點oldCh[idxInOld]要設置爲undefined,方便下次循環處理時過濾掉已經處理的節點
            oldCh[idxInOld] = undefined as any
            // 將elmToMove.elm(例子中舊的key爲B的節點)插入到oldStartVnode.elm(例子中key爲A的節點)的前面
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        // 取newCh[++newStartIdx]虛擬節點(例子中key爲D的虛擬節點)賦值給newStartVnode
        newStartVnode = newCh[++newStartIdx]
      }
    }
    /**
     * 循環結束後舊的前索引 <= 舊的後索引 || 新的前索引 <= 新的後索引,
     * 表示還有部分虛擬節點(例子中key爲C的虛擬節點)沒處理
     * 舊的虛擬子節點                   新的虛擬子節點
     * 狀況一:
     * h('li', { key: 'A' }, 'A')      h('li', { key: 'A' }, 'A')
     * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')
     * h('li', { key: 'D' }, 'D')      h('li', { key: 'C' }, 'C')
     *                                 h('li', { key: 'D' }, 'D')
     * 狀況二:
     * h('li', { key: 'A' }, 'A')      h('li', { key: 'A' }, 'A')
     * h('li', { key: 'B' }, 'B')      h('li', { key: 'B' }, 'B')
     * h('li', { key: 'C' }, 'C')
    */
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      // 處理例子中狀況一
      if (oldStartIdx > oldEndIdx) {
        // 待插入的節點以before節點爲參照,newCh[newEndIdx]是例子中新的子節點中key爲C的虛擬節點,
        // 因此before = newCh[newEndIdx + 1]是key爲D的虛擬節點
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        // 例子中如今newStartIdx,newEndIdx都爲2
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else {
        // 處理例子中狀況二,刪除舊的前索引到舊的後索引中間的節點(例子中刪除舊的key爲C的虛擬節點)
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
  }
相關文章
相關標籤/搜索