圖文並茂地來詳細講講Vue Diff算法

這是我參與更文挑戰的第2天,活動詳情查看: 更文挑戰javascript

最近恰好看完Vue源碼中的Diff算法,恰好在參加更文挑戰,就作了一些動圖還有流程圖,圖文並茂地來詳細講一講,VueDiff算法叭。html

Vue2是如何更新節點

咱們都知道,Vue中是使用了基於HTML的模板語法,容許開發者聲明式地將DOM綁定至底層Vue實例的數據。而初始化的時候,Vue就會將該模板語法轉化爲真實DOM,渲染到頁面中。vue

<template>
	<div>{{msg}}</div>
</template>

<script> export.default { data() { return { msg: 'HelloWorld' } } } </script>
複製代碼

但當數據發生變化的時候,Vue會如何去更新頁面呢?java

若是選擇從新渲染整個DOM,那必然會引發整個DOM樹的重繪和重排,而在真實項目中,不可能就跟上面的例子同樣只有一句<div>{{msg}}</div>。當咱們的頁面很是複雜的狀況下,且修改的數據隻影響到一小部分頁面數據的更新的時候,從新渲染頁面必定是不可取的。node

而這時候,最便捷的方式,就是找到該修改的數據所影響到的DOM,而後只更新那一個DOM就能夠了。這就是Vue更新頁面的方法。git

Vue在初始化頁面後,會將當前的真實DOM轉換爲虛擬DOM(Virtual DOM),並將其保存起來,這裏稱爲oldVnode。而後當某個數據發變化後,Vue會先生成一個新的虛擬DOM——vnode,而後將vnodeoldVnode進行比較,找出須要更新的地方,而後直接在對應的真實DOM上進行修改。當修改結束後,就將vnode賦值給oldVnode存起來,做爲下次更新比較的參照物。github

而這個更新中的難點,也是咱們今天要聊的內容,就是新舊vnode的比較,也就是咱們常說的Diff算法。web

什麼是虛擬DOM

前面咱們提到了虛擬DOM(Virtual DOM),那虛擬DOM是什麼呢?算法

咱們可能曾經打印過真實DOM,它實質上是個對象,可是它的元素是很是的多的,即便是很簡單的幾句代碼。json

dom.png

所以,在真實DOM下,咱們不太敢隨便去直接操做和改動。

這時候,虛擬DOM就誕生了。它也是一個對象,而它實際上是將真實DOM的數據抽取出來,以對象的形式模擬樹形結構,使其更加簡潔明瞭。

虛擬DOM沒有很固定的模板,每一個框架上的實現都存在差別,可是大部分結構都是相同的。下面咱們就用Vue的虛擬DOM舉個例子。

<div id="app">
  		<p class="text">HelloWorld</p>
</div>
複製代碼

上面的DOM經過Vue生成了下面的虛擬DOM(有刪減),對象中包含了根節點的標籤tagkey值,文本信息text等等,同時也含有elm屬性存放真實DOM,同時有個children數組,存放着子節點,子節點的結構也是一致的。

{
    "tag": "div", // 標籤
    "key": undefined,    // key值
    "elm": div#app,      // 真實DOM
    "text": undefined,   // 文本信息
    "data": {attrs: {id:"app"}},      // 節點屬性
    "children": [{    	// 孩子屬性
        "tag": "p",
        "key": undefined,
        "elm": p.text,
        "text": undefined,
        "data": {attrs: {class: "text"}},
        "children": [{
            "tag": undefined,
            "key": undefined,
            "elm": text,
            "text": "helloWorld",
            "data": undefined,
            "children": []
        }]
    }]
}
複製代碼

當咱們把一些經常使用的信息提取出來,而且使用對象嵌套的形式,去存放子節點信息,從而造成一個虛擬DOM,這時候咱們用其來進行比較的話,就會比兩個真實DOM作比較簡單多了。

Vue中,有個render函數,這個函數返回的VNode就是一個虛擬DOM。固然,你也可使用virtual-domsnabbdom去體驗一下虛擬DOM

Diff的實現

在使用Diff算法比較兩個節點的時候,只會在同層級進行比較,而不會跨層級比較。

diff.gif

流程

Vue中,主要是patch()patchVnode()updateChildren()這三個主要方法來實現Diff的。

  • 當咱們Vue中的響應式數據變化的時候,就會觸發頁面更新函數updateComponent()(如何觸發能夠經過閱讀Vue源碼進行學習或者看一下我以前一篇《簡單手寫實現Vue2.x》);
  • 此時updateComponet()就會調用patch()方法,在該方法中進行比較是否爲相同節點,是的話執行patchVnode()方法,開始比較節點差別;而若是不是相同節點的話,則進行替換操做,具體後面會講到;
  • patchVnode()中,首先是更新節點屬性,而後會判斷有沒有孩子節點,有的話則執行updateChildren()方法,對孩子節點進行比較;若是沒有孩子節點的話,則進行節點文本內容判斷更新;(文本節點是不會有孩子節點的)
  • updateChildren()中,會對傳入的兩個孩子節點數組進行一一比較,當找到相同節點的狀況下,調用patchVnode()繼續節點差別比較。

diff.jpg

準備工做

爲了後面更好的看核心代碼,咱們先在前面捋清楚一些函數。

isDef 和 isUndef

在源碼中會用isDef()isUndef()判斷vnode是否存在,實質上是判斷vnode是否是undefinednull,畢竟vnode虛擬DOM是個對象。

export function isUndef (v: any): boolean %checks {
  return v === undefined || v === null
}

export function isDef (v: any): boolean %checks {
  return v !== undefined && v !== null
}
複製代碼

sameVnode

在源碼中會用sameVnode()方法去判斷兩個節點是否相同,實質上是經過去判斷key值,tag標籤等靜態屬性從而去判斷兩個節點是否爲相同節點。

注意的是,這裏的相同節點不意味着爲相等節點,好比<div>HelloWorld</div><div>HiWorld</div>爲相同節點,可是它們並不相等。在源碼中是經過vnode1 === vnode2去判斷是否是爲相等節點。

// 比較是否相同節點
function sameVnode(a, b) {
    return (
        a.key === b.key &&
        a.asyncFactory === b.asyncFactory && (
            (
                a.tag === b.tag &&
                a.isComment === b.isComment &&
                isDef(a.data) === isDef(b.data) &&
                sameInputType(a, b)
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

function sameInputType(a, b) {
    if (a.tag !== 'input') return true
    let i
    const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
    const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
複製代碼

patch

接下來開始看源碼了,看看Vue是如何實現Diff算法。

下面的全部代碼都只保留核心的代碼,想看所有代碼能夠去看Vue的源碼(patch文件路徑:github.com/vuejs/vue/b…

首先看看patch()方法,該方法接收新舊虛擬Dom,即oldVnodevnode,這個函數實際上是對新舊虛擬Dom作一個簡單的判斷,而尚未進入詳細的比較階段。

  • 首先判斷vnode是否存在,若是不存在的話,則表明這個舊節點要整個刪除;
  • 若是vnode存在的話,再判斷oldVnode是否存在,若是不存在的話,則表明只須要新增整個vnode節點就能夠;
  • 若是vnodeoldVnode都存在的話,判斷二者是否是相同節點,若是是的話,這調用patchVnode方法,對兩個節點進行詳細比較判斷;
  • 若是二者不是相同節點的話,這種狀況通常就是初始化頁面,此時oldVnode實際上是真實Dom,這是隻須要將vnode轉換爲真實Dom而後替換掉oldVnode,具體就很少講,這不是今天討論的範圍內。
// 更新時調用的__patch__
function patch(oldVnode, vnode, hydrating, removeOnly) {
    // 判斷新節點是否存在
    if (isUndef(vnode)) {
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)  // 新的節點不存在且舊節點存在:刪除
        return
    }

		// 判斷舊節點是否存在
    if (isUndef(oldVnode)) {
				// 舊節點不存在且新節點存在:新增
        createElm(vnode, insertedVnodeQueue)  
    } else {
        if (sameVnode(oldVnode, vnode)) {
            // 比較新舊節點 diff算法
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        } else {
            // 初始化頁面(此時的oldVnode是個真實DOM)
                oldVnode = emptyNodeAt(oldVnode)
            }
            // 建立新的節點
            createElm(
                vnode,
                insertedVnodeQueue,
                oldElm._leaveCb ? null : parentElm,
                nodeOps.nextSibling(oldElm)
            )
        }
    }

    return vnode.elm
}
複製代碼

diff2.jpg

patchVnode

patchVnode()中,一樣是接收新舊虛擬Dom,即oldVnodevnode;在該函數中,即開始對兩個虛擬Dom進行比較更新了。

  • 首先判斷兩個虛擬Dom是否是全等,即沒有任何變更;是的話直接結束函數,不然繼續執行;
  • 其次更新節點的屬性;
  • 接着判斷vnode.text是否存在,即vnode是否是文本節點。是的話,只須要更新節點文本既可,不然的話,這繼續比較;
  • 判斷vnodeoldVnode是否有孩子節點:
    • 若是二者都有孩子節點的話,執行updateChildren()方法,進行比較更新孩子節點;
    • 若是vnode有孩子節點而oldVnode沒有的話,則直接新增全部孩子節點,並將該節點文本屬性設爲空;
    • 若是oldVnode有孩子節點而vnode沒有的話,則直接刪除全部孩子節點;
    • 若是二者都沒有孩子節點,就判斷oldVnode.text是否有內容,有的話清空內容既可。
// 比較兩個虛擬DOM
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    // 若是兩個虛擬DOM同樣,無需比較直接返回
    if (oldVnode === vnode) {
        return
    }

    // 獲取真實DOM
    const elm = vnode.elm = oldVnode.elm

    // 獲取兩個比較節點的孩子節點
    const oldCh = oldVnode.children
    const ch = vnode.children

    // 屬性更新
    if (isDef(data) && isPatchable(vnode)) {
        for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
        if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }

    if (isUndef(vnode.text)) {   // 沒有文本 -> 該狀況通常都是有孩子節點
        if (isDef(oldCh) && isDef(ch)) {  // 新舊節點都有孩子節點 -> 比較子節點
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {  // 新節點有孩子節點,舊節點沒有孩子節點 -> 新增
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')  // 若是舊節點有文本內容,將其設置爲空
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) {   // 舊節點有孩子節點,新節點沒有孩子節點 -> 刪除
            removeVnodes(oldCh, 0, oldCh.length - 1)
        } else if (isDef(oldVnode.text)) {   // 舊節點有文本,新節點沒有文本 -> 刪除文本
            nodeOps.setTextContent(elm, '')
        }
    } else if (oldVnode.text !== vnode.text) {  // 新舊節點文本不一樣 -> 更新文本
        nodeOps.setTextContent(elm, vnode.text)
    }
}
複製代碼

diff3.jpg

updateChildren

最後就來看看updateChildren方法了,這個也是最難理解的一部分,因此就先帶你們一步步捋清楚後,手寫一下,再看源碼。

首先這個方法傳入三個比較重要的參數,即parentElm父級真實節點,便於直接節點操做;oldCholdVnode的孩子節點;newChVnode的孩子節點。

oldChnewCh都是一個數組。 這個方法的做用,就是對這兩個數組一一比較,找到相同的節點,執行patchVnode再次進行比較更新,剩下的少退多補。

這個方法咱們想到最簡單的方法,就是兩個數組進行遍歷匹配,可是這樣子的複雜度是很大的,時間複雜度爲O(NM),並且咱們真實項目中,頁面結構是很是龐大和複雜的,因此這個方案是很是耗性能的。

Vue中,主要的實現是用四個指針進行實現。四個指針初始位置分別在兩個數組的頭尾。所以咱們先來初始化必要的變量。

let oldStartIdx = 0;   								// oldCh數組左邊的指針位置
let oldStartVnode = oldCh[0];  			  // oldCh數組左邊的指針對應的節點
let oldEndIdx = oldCh.length - 1; 		// oldCh數組右邊的指針位置
let oldEndVnode = oldCh[oldEndIdx]; 	// oldCh數組右邊的指針對應的節點
let newStartIdx = 0;									// newCh數組左邊的指針位置
let newStartVnode = newCh[0];					// newCh數組左邊的指針對應的節點
let newEndIdx = newCh.length - 1;			// newCh數組右邊的指針位置
let newEndVnode = newCh[newEndIdx]; 	// newCh數組右邊的指針對應的節點
複製代碼

diff4.jpeg

然而這四個指針不會一直不動的,它們會進行相互比較,若是比較得出是相同節點後,對應兩個指針就會向另外一側移動,而直至兩兩重合的時候,這個循環也就結束了。

固然看到這裏,你會有不少疑問,但先把疑問記起來,後面都會一一做答的。咱們接着寫一個循環語句。

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // TODO
}
複製代碼

接着咱們開始相互比較。

首先是oldStartVnodenewStartVnode進行比較,若是比較相同的話,咱們就能夠執行patchVnode語句,而且移動oldStartIdxnewStartIdx

diff2.gif

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
      }
}
複製代碼

若是oldStartVnodenewStartVnode匹配不上的話,接下來就是oldEndVnodenewEndVnode作比較了。

diff3.gif

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
      }
}
複製代碼

但若是兩頭比較和兩尾比較都不是相同節點的話,這時候就開始交叉比較了。首先是oldStartVnodenewEndVnode作比較。

diff4.gif

但交叉比較的時候若是匹配上的話,就須要注意到一個問題,這時候你不只僅要比較更新節點的內容,你還須要移動節點的位置,所以咱們能夠藉助insertBeforenextSiblingDOM操做方法去實現,這個自行去學習叭。

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            // 將oldStartVnode節點移動到對應位置,即oldEndVnode節點的後面
            nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
     
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
      }
}
複製代碼

若是oldStartVnodenewEndVnode匹配不上的話,就oldEndVnodenewStartVnode進行比較。

diff5.gif

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 將oldEndVnode節點移動到對應位置,即oldStartVnode節點的前面
            nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
      }
}
複製代碼

此時,若是四種比較方法都匹配不到相同節點的話,咱們就只能使用暴力解法去實現了,也就是針對於newStartVnode這個節點,咱們去遍歷oldCh中剩餘的節點,一一匹配。

Vue中,咱們知道標籤會有一個屬性——key值,而在同一級的Dom中,若是key有值的話,它必須是惟一的;若是不設值就默認爲undefined。因此咱們能夠先用key來配對一下。

咱們能夠先生成一個oldChkey->index的映射表,咱們能夠建立一個函數createKeyToOldIdx實現,返回的結果用一個變量oldKeyToIdx去存儲。

function createKeyToOldIdx(children, beginIdx, endIdx) {
    let i, key
    const map = {}
    for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
    }
    return map
}
複製代碼

這時候,若是newStartVnode存在key的話,咱們就能夠直接用oldKeyToIdx[newStartVnode.key]拿到對應舊孩子節點的下標index

但若是newStartVnode沒有key值的話,就只能經過遍歷oldCh中剩餘的節點,一一進行匹配獲取對應下標index,這個也能夠封裝成一個函數去實現。

function findIdxInOld(node, oldCh, start, end) {
    for (let i = start; i < end; i++) {
        const c = oldCh[i]
        if (isDef(c) && sameVnode(node, c)) return i
    }
}
複製代碼

這時候咱們先繼續手寫代碼。

let oldKeyToIdx, idxInOld;

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            ...
      }else{
        		// 遍歷剩餘的舊孩子節點,將有key值的生成index表 <{key: i}>
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            // 若是newStartVnode存在key,就進行匹配index值;若是沒有key值,遍歷剩餘的舊孩子節點,一一與newStartVnode匹配,相同節點的返回index
            idxInOld = isDef(newStartVnode.key)
                ? oldKeyToIdx[newStartVnode.key]
                : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      }
}
複製代碼

固然,這個狀況下,idxInOld下標值仍是有可能爲空,這種狀況就表明那個newStartVnode是一個全新的節點,這時候咱們只須要新增節點就能夠了。

若是idxInOld不爲空的話,咱們就獲取對應的oldVnode,而後與newStartVnode進行比較,若是是相同節點的話,調用patchVnode()函數, 而且將對應的oldVnode設置爲undefined;若是匹配出來時不一樣節點,那就直接建立一個節點既可。

最後,移動一下newStartIdx

let oldKeyToIdx, idxInOld, vnodeToMove;

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            ...
      }else{
        		// 遍歷剩餘的舊孩子節點,將有key值的生成index表 <{key: i}>
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            // 若是newStartVnode存在key,就進行匹配index值;若是沒有key值,遍歷剩餘的舊孩子節點,一一與newStartVnode匹配,相同節點的返回index
            idxInOld = isDef(newStartVnode.key)
                ? oldKeyToIdx[newStartVnode.key]
                : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { 
                // 若是匹配不到index,則建立新節點
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                // 獲取對應的舊孩子節點
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                    // 由於idxInOld是處於oldStartIdx和oldEndIdx之間,所以只能將其設置爲undefined,而不是移動兩個指針
                    oldCh[idxInOld] = undefined
                    nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // 若是key相同但節點不一樣,就建立一個新的節點
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
                }
            }
            // 移動新節點的左邊指針
            newStartVnode = newCh[++newStartIdx]
      }
}
複製代碼

diff6.gif

這裏有個重點,若是咱們匹配到對應的oldVnode的話,須要將其設置爲undefined,同時當後面咱們的oldStartIdxoldEndIdx移動後,若是判斷出對應的vnodeundefined時,就須要選擇跳過。

let oldKeyToIdx, idxInOld, vnodeToMove;

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        		// 當oldStartVnode爲undefined的時候,oldStartVnode右移
        		oldStartVnode = oldCh[++oldStartIdx] 
      } else if (isUndef(oldEndVnode)) {
        		// 當oldEndVnode爲undefined的時候,oldEndVnode左移
        		oldEndVnode = oldCh[--oldEndIdx]
      } else if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            ...
      }else{
        		...
      }
}
複製代碼

diff7.gif

diff8.gif

到這個時候,咱們已經完成的差很少了,只剩下最後的收尾工做了。

若是這時候,oldCh的兩個指針已經重疊並越過,而newCh的兩個指針還未重疊;或者說是相反狀況下。

diff5.jpeg

這時候,若是oldCh有多餘的vnode,咱們只須要將其都刪除既可;若是是newCh有多餘的vnode,咱們只需新增它們就能夠了。

let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        		...
      } else if (isUndef(oldEndVnode)) {
        		...
      } else if(sameVnode(oldStartVnode, newStartVnode)){
        		...
      }else if(sameVnode(oldEndVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldStartVnode, newEndVnode)){
            ...
      }else if(sameVnode(oldEndVnode, newStartVnode)){
            ...
      }else{
        		...
      }
            
      if (oldStartIdx > oldEndIdx) {
            // 當舊節點左指針已經超過右指針的時候,新增剩餘的新的孩子節點
            refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
            addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    	} else if (newStartIdx > newEndIdx) {
            // 當新節點左指針已經超過右指針的時候,刪除剩餘的舊的孩子節點
            removeVnodes(oldCh, oldStartIdx, oldEndIdx)
   		}
}
複製代碼

這時候,咱們就完成了updateChildren()方法了,總體代碼以下:

// 比較兩組孩子節點
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 設置首尾4個指針和對應節點
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]

    // diff查找是所需的變量
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // 循環結束條件:新舊節點的頭尾指針都重合
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            // 當oldStartVnode爲undefined的時候,oldStartVnode右移
            oldStartVnode = oldCh[++oldStartIdx] 
        } else if (isUndef(oldEndVnode)) {
            // 當oldEndVnode爲undefined的時候,oldEndVnode左移
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // 當oldStartVnode與newStartVnode節點相同,對比節點
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 對應兩個指針更新
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // 當oldEndVnode與newEndVnode節點相同,對比節點
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            // 對應兩個指針更新
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // 當oldStartVnode與newEndVnode節點相同,對比節點
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            // 將oldStartVnode節點移動到對應位置,即oldEndVnode節點的後面
            nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            // 對應兩個指針更新
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // 當oldEndVnode與newStartVnode節點相同,對比節點
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 將oldEndVnode節點移動到對應位置,即oldStartVnode節點的前面
            nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            // 對應兩個指針更新
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {   // 暴力解法 使用key匹配
            // 遍歷剩餘的舊孩子節點,將有key值的生成index表 <{key: i}>
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            // 若是newStartVnode存在key,就進行匹配index值;若是沒有key值,遍歷剩餘的舊孩子節點,一一與newStartVnode匹配,相同節點的返回index
            idxInOld = isDef(newStartVnode.key)
                ? oldKeyToIdx[newStartVnode.key]
                : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

            if (isUndef(idxInOld)) { 
                // 若是匹配不到index,則建立新節點
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                // 獲取對應的舊孩子節點
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                    // 由於idxInOld是處於oldStartIdx和oldEndIdx之間,所以只能將其設置爲undefined,而不是移動兩個指針
                    oldCh[idxInOld] = undefined
                    nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // 若是key相同但節點不一樣,就建立一個新的節點
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
                }
            }
            // 移動新節點的左邊指針
            newStartVnode = newCh[++newStartIdx]
        }
    }

    if (oldStartIdx > oldEndIdx) {
        // 當舊節點左指針已經超過右指針的時候,新增剩餘的新的孩子節點
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
        // 當新節點左指針已經超過右指針的時候,刪除剩餘的舊的孩子節點
        removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
}
複製代碼

流程圖

diff6.png

爲何要用key值

咱們在前面的sameVnode()能夠看到,咱們在比較兩個節點是否相同的時候,第一個判斷條件就是vnode.key;而且在後面使用暴力解法的時候,第一選擇也是經過key去匹配,而這樣會有什麼好處呢?咱們經過下面一個簡單的例子來解答這個問題叭。

假設咱們此時的新舊節點以下:

<!-- old -->
<div>
  	<p>A</p>
  	<p>B</p>
  	<p>C</p>
</div>

<!-- new -->
<div>
  	<p>B</p>
  	<p>C</p>
  	<p>A</p>
</div>
複製代碼

在上面的例子,咱們能夠看出,<p>A</p>被移動到最後面去了。

但若是咱們沒有設置key值的話,經過diff須要操做Dom的次數會不少,由於當keyundefined的狀況下,每一個p標籤其實都是相同節點,所以這是執行diff的話,它會將第一個A改爲B,把第二個B改爲C,把第三個C改爲A,這時一共操做了三次Dom

diff9.gif

但若是,咱們分別給對應添加了key值,經過diff只需操做一次Dom,即將第一個節點移動到最後既可。

diff10.gif

相關文章
相關標籤/搜索