溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎樣深入理解vue中的虛擬DOM和Diff算法

發(fā)布時間:2021-12-18 16:35:20 來源:億速云 閱讀:236 作者:柒染 欄目:編程語言

怎樣深入理解vue中的虛擬DOM和Diff算法,針對這個問題,這篇文章詳細介紹了相對應(yīng)的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。

真實DOM的渲染

在講虛擬DOM之前,先說一下真實DOM的渲染。

怎樣深入理解vue中的虛擬DOM和Diff算法

瀏覽器真實DOM渲染的過程大概分為以下幾個部分

  • 構(gòu)建DOM樹。通過HTML parser解析處理HTML標(biāo)記,將它們構(gòu)建為DOM樹(DOM tree),當(dāng)解析器遇到非阻塞資源(圖片,css),會繼續(xù)解析,但是如果遇到script標(biāo)簽(特別是沒有async 和 defer屬性),會阻塞渲染并停止html的解析,這就是為啥最好把script標(biāo)簽放在body下面的原因。

  • 構(gòu)建CSSOM樹。與構(gòu)建DOM類似,瀏覽器也會將樣式規(guī)則,構(gòu)建成CSSOM。瀏覽器會遍歷CSS中的規(guī)則集,根據(jù)css選擇器創(chuàng)建具有父子,兄弟等關(guān)系的節(jié)點樹。

  • 構(gòu)建Render樹。這一步將DOM和CSSOM關(guān)聯(lián),確定每個 DOM 元素應(yīng)該應(yīng)用什么 CSS 規(guī)則。將所有相關(guān)樣式匹配到DOM樹中的每個可見節(jié)點,并根據(jù)CSS級聯(lián)確定每個節(jié)點的計算樣式,不可見節(jié)點(head,屬性包括 display:none的節(jié)點)不會生成到Render樹中。

  • 布局/回流(Layout/Reflow)。瀏覽器第一次確定節(jié)點的位置以及大小叫布局,如果后續(xù)節(jié)點位置以及大小發(fā)生變化,這一步觸發(fā)布局調(diào)整,也就是 回流。

  • 繪制/重繪(Paint/Repaint)。將元素的每個可視部分繪制到屏幕上,包括文本、顏色、邊框、陰影和替換的元素(如按鈕和圖像)。如果文本、顏色、邊框、陰影等這些元素發(fā)生變化時,會觸發(fā)重繪(Repaint)。為了確保重繪的速度比初始繪制的速度更快,屏幕上的繪圖通常被分解成數(shù)層。將內(nèi)容提升到GPU層(可以通過tranform,filter,will-change,opacity觸發(fā))可以提高繪制以及重繪的性能。

  • 合成(Compositing)。這一步將繪制過程中的分層合并,確保它們以正確的順序繪制到屏幕上顯示正確的內(nèi)容。

為啥需要虛擬DOM

上面這是一次DOM渲染的過程,如果dom更新,那么dom需要重新渲染一次,如果存在下面這種情況

<body>
    <div id="container">
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
				....
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
    </div>
</body>
<script>
    let content = document.getElementsByClassName('content');
    for (let i = 0; i < 1000000; i++) {
        content[i].innerHTML = `This is a content${i}`;
        // 觸發(fā)回流
        content[i].style.fontSize = `20px`;
    }
</script>

那么需要真實的操作DOM100w次,觸發(fā)了回流100w次。每次DOM的更新都會按照流程進行無差別的真實dom的更新。所以造成了很大的性能浪費。如果循環(huán)里面是復(fù)雜的操作,頻繁觸發(fā)回流與重繪,那么就很容易就影響性能,造成卡頓。另外這里要說明一下的是,虛擬DOM并不是意味著比DOM就更快,性能需要分場景,虛擬DOM的性能跟模板大小是正相關(guān)。虛擬DOM的比較過程是不會區(qū)分數(shù)據(jù)量大小的,在組件內(nèi)部只有少量動態(tài)節(jié)點時,虛擬DOM依然是會對整個vdom進行遍歷,相比直接渲染而言是多了一層操作的。

<div class="list">
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">{{ item }}</p>
    <p class="item">item</p>
    <p class="item">item</p>
  </div>

比如上面這個例子,虛擬DOM。雖然只有一個動態(tài)節(jié)點,但是虛擬DOM依然需要遍歷diff整個list的class,文本,標(biāo)簽等信息,最后依然需要進行DOM渲染。如果只是dom操作,就只要操作一個具體的DOM然后進行渲染。虛擬DOM最核心的價值在于,它能通過js描述真實DOM,表達力更強,通過聲明式的語言操作,為開發(fā)者提供了更加方便快捷開發(fā)體驗,而且在沒有手動優(yōu)化,大部分情景下,保證了性能下限,性價比更高。

虛擬DOM

虛擬DOM本質(zhì)上是一個js對象,通過對象來表示真實的DOM結(jié)構(gòu)。tag用來描述標(biāo)簽,props用來描述屬性,children用來表示嵌套的層級關(guān)系。

const vnode = {
    tag: 'div',
    props: {
        id: 'container',
    },
    children: [{
        tag: 'div',
        props: {
            class: 'content',
        },
      	text: 'This is a container'
    }]
}

//對應(yīng)的真實DOM結(jié)構(gòu)
<div id="container">
  <div class="content">
    This is a container
  </div>
</div>

虛擬DOM的更新不會立即操作DOM,而是會通過diff算法,找出需要更新的節(jié)點,按需更新,并將更新的內(nèi)容保存為一個js對象,更新完成后再掛載到真實dom上,實現(xiàn)真實的dom更新。通過虛擬DOM,解決了操作真實DOM的三個問題。

  • 無差別頻繁更新導(dǎo)致DOM頻繁更新,造成性能問題

  • 頻繁回流與重繪

  • 開發(fā)體驗

另外由于虛擬DOM保存的是js對象,天然的具有跨平臺的能力,而不僅僅局限于瀏覽器。

優(yōu)點

總結(jié)起來,虛擬DOM的優(yōu)勢有以下幾點

  • 小修改無需頻繁更新DOM,框架的diff算法會自動比較,分析出需要更新的節(jié)點,按需更新

  • 更新數(shù)據(jù)不會造成頻繁的回流與重繪

  • 表達力更強,數(shù)據(jù)更新更加方便

  • 保存的是js對象,具備跨平臺能力

不足

虛擬DOM同樣也有缺點,首次渲染大量DOM時,由于多了一層虛擬DOM的計算,會比innerHTML插入慢。

虛擬DOM實現(xiàn)原理

主要分三部分

  • 通過js建立節(jié)點描述對象

  • diff算法比較分析新舊兩個虛擬DOM差異

  • 將差異patch到真實dom上實現(xiàn)更新

Diff算法

為了避免不必要的渲染,按需更新,虛擬DOM會采用Diff算法進行虛擬DOM節(jié)點比較,比較節(jié)點差異,從而確定需要更新的節(jié)點,再進行渲染。vue采用的是深度優(yōu)先,同層比較的策略。

怎樣深入理解vue中的虛擬DOM和Diff算法

新節(jié)點與舊節(jié)點的比較主要是圍繞三件事來達到渲染目的

  • 創(chuàng)建新節(jié)點

  • 刪除廢節(jié)點

  • 更新已有節(jié)點

如何比較新舊節(jié)點是否一致呢?

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) //對input節(jié)點的處理
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

//判斷兩個節(jié)點是否是同一種 input 輸入類型
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
    //input type 相同或者兩個type都是text
    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

可以看到,兩個節(jié)點是否相同是需要比較標(biāo)簽(tag),屬性(在vue中是用data表示vnode中的屬性props), 注釋節(jié)點(isComment) 的,另外碰到input的話,是會做特殊處理的。

創(chuàng)建新節(jié)點

當(dāng)新節(jié)點有的,舊節(jié)點沒有,這就意味著這是全新的內(nèi)容節(jié)點。只有元素節(jié)點,文本節(jié)點,注釋節(jié)點才能被創(chuàng)建插入到DOM中。

刪除舊節(jié)點

當(dāng)舊節(jié)點有,而新節(jié)點沒有,那就意味著,新節(jié)點放棄了舊節(jié)點的一部分。刪除節(jié)點會連帶的刪除舊節(jié)點的子節(jié)點。

更新節(jié)點

新的節(jié)點與舊的的節(jié)點都有,那么一切以新的為準(zhǔn),更新舊節(jié)點。如何判斷是否需要更新節(jié)點呢?

  • 判斷新節(jié)點與舊節(jié)點是否完全一致,一樣的話就不需要更新

  // 判斷vnode與oldVnode是否完全一樣
  if (oldVnode === vnode) {
    return;
  }
  • 判斷新節(jié)點與舊節(jié)點是否是靜態(tài)節(jié)點,key是否一樣,是否是克隆節(jié)點(如果不是克隆節(jié)點,那么意味著渲染函數(shù)被重置了,這個時候需要重新渲染)或者是否設(shè)置了once屬性,滿足條件的話替換componentInstance

  // 是否是靜態(tài)節(jié)點,key是否一樣,是否是克隆節(jié)點或者是否設(shè)置了once屬性
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }
  • 判斷新節(jié)點是否有文本(通過text屬性判斷),如果有文本那么需要比較同層級舊節(jié)點,如果舊節(jié)點文本不同于新節(jié)點文本,那么采用新的文本內(nèi)容。如果新節(jié)點沒有文本,那么后面需要對子節(jié)點的相關(guān)情況進行判斷

//判斷新節(jié)點是否有文本
if (isUndef(vnode.text)) {
  //如果沒有文本,處理子節(jié)點的相關(guān)代碼
  ....
} else if (oldVnode.text !== vnode.text) {
  //新節(jié)點文本替換舊節(jié)點文本
  nodeOps.setTextContent(elm, vnode.text)
}
  • 判斷新節(jié)點與舊節(jié)點的子節(jié)點相關(guān)狀況。這里又能分為4種情況

    • 新節(jié)點與舊節(jié)點都有子節(jié)點

    • 只有新節(jié)點有子節(jié)點

    • 只有舊節(jié)點有子節(jié)點

    • 新節(jié)點與舊節(jié)點都沒有子節(jié)點

都有子節(jié)點

對于都有子節(jié)點的情況,需要對新舊節(jié)點做比較,如果他們不相同,那么需要進行diff操作,在vue中這里就是updateChildren方法,后面會詳細再講,子節(jié)點的比較主要是雙端比較。

//判斷新節(jié)點是否有文本
if (isUndef(vnode.text)) {
    //新舊節(jié)點都有子節(jié)點情況下,如果新舊子節(jié)點不相同,那么進行子節(jié)點的比較,就是updateChildren方法
    if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    }
} else if (oldVnode.text !== vnode.text) {
    //新節(jié)點文本替換舊節(jié)點文本
    nodeOps.setTextContent(elm, vnode.text)
}

只有新節(jié)點有子節(jié)點

只有新節(jié)點有子節(jié)點,那么就代表著這是新增的內(nèi)容,那么就是新增一個子節(jié)點到DOM,新增之前還會做一個重復(fù)key的檢測,并做出提醒,同時還要考慮,舊節(jié)點如果只是一個文本節(jié)點,沒有子節(jié)點的情況,這種情況下就需要清空舊節(jié)點的文本內(nèi)容。

//只有新節(jié)點有子節(jié)點
if (isDef(ch)) {
  //檢查重復(fù)key
  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(ch)
  }
  //清除舊節(jié)點文本
  if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
  //添加新節(jié)點
  addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}

//檢查重復(fù)key
function checkDuplicateKeys(children) {
  const seenKeys = {}
  for (let i = 0; i < children.length; i++) {
      const vnode = children[i]
      //子節(jié)點每一個Key
      const key = vnode.key
      if (isDef(key)) {
          if (seenKeys[key]) {
              warn(
                  `Duplicate keys detected: '${key}'. This may cause an update error.`,
                  vnode.context
              )
          } else {
              seenKeys[key] = true
          }
      }
  }
}

只有舊節(jié)點有子節(jié)點

只有舊節(jié)點有,那就說明,新節(jié)點拋棄了舊節(jié)點的子節(jié)點,所以需要刪除舊節(jié)點的子節(jié)點

if (isDef(oldCh)) {
  //刪除舊節(jié)點
  removeVnodes(oldCh, 0, oldCh.length - 1)
}

都沒有子節(jié)點

這個時候需要對舊節(jié)點文本進行判斷,看舊節(jié)點是否有文本,如果有就清空

if (isDef(oldVnode.text)) {
  //清空
  nodeOps.setTextContent(elm, '')
}

整體的邏輯代碼如下

function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
) {
    // 判斷vnode與oldVnode是否完全一樣
    if (oldVnode === vnode) {
        return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
        // 克隆重用節(jié)點
        vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
        if (isDef(vnode.asyncFactory.resolved)) {
            hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
        } else {
            vnode.isAsyncPlaceholder = true
        }
        return
    }
		// 是否是靜態(tài)節(jié)點,key是否一樣,是否是克隆節(jié)點或者是否設(shè)置了once屬性
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
        vnode.componentInstance = oldVnode.componentInstance
        return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
        i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children

    if (isDef(data) && isPatchable(vnode)) {
      	//調(diào)用update回調(diào)以及update鉤子
        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)
    }
    //判斷新節(jié)點是否有文本
    if (isUndef(vnode.text)) {
      	//新舊節(jié)點都有子節(jié)點情況下,如果新舊子節(jié)點不相同,那么進行子節(jié)點的比較,就是updateChildren方法
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {
          	//只有新節(jié)點有子節(jié)點
            if (process.env.NODE_ENV !== 'production') {
              	//重復(fù)Key檢測
                checkDuplicateKeys(ch)
            }
          	//清除舊節(jié)點文本
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
          	//添加新節(jié)點
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) {
          	//只有舊節(jié)點有子節(jié)點,刪除舊節(jié)點
            removeVnodes(oldCh, 0, oldCh.length - 1)
        } else if (isDef(oldVnode.text)) {
          	//新舊節(jié)點都無子節(jié)點
            nodeOps.setTextContent(elm, '')
        }
    } else if (oldVnode.text !== vnode.text) {
      	//新節(jié)點文本替換舊節(jié)點文本
        nodeOps.setTextContent(elm, vnode.text)
    }

    if (isDef(data)) {
        if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
}

配上流程圖會更清晰點

怎樣深入理解vue中的虛擬DOM和Diff算法

怎樣深入理解vue中的虛擬DOM和Diff算法

子節(jié)點的比較更新updateChildren

新舊節(jié)點都有子節(jié)點的情況下,這個時候是需要調(diào)用updateChildren方法來比較更新子節(jié)點的。所以在數(shù)據(jù)上,新舊節(jié)點子節(jié)點,就保存為了兩個數(shù)組。

const oldCh = [oldVnode1, oldVnode2,oldVnode3];
const newCh = [newVnode1, newVnode2,newVnode3];

子節(jié)點更新采用的是雙端比較的策略,什么是雙端比較呢,就是新舊節(jié)點比較是通過互相比較首尾元素(存在4種比較),然后向中間靠攏比較(newStartIdx,與oldStartIdx遞增,newEndIdx與oldEndIdx遞減)的策略。

比較過程

怎樣深入理解vue中的虛擬DOM和Diff算法

向中間靠攏

怎樣深入理解vue中的虛擬DOM和Diff算法

這里對上面出現(xiàn)的新前,新后,舊前,舊后做一下說明

  • 新前,指的是新節(jié)點未處理的子節(jié)點數(shù)組中的第一個元素,對應(yīng)到vue源碼中的newStartVnode

  • 新后,指的是新節(jié)點未處理的子節(jié)點數(shù)組中的最后一個元素,對應(yīng)到vue源碼中的newEndVnode

  • 舊前,指的是舊節(jié)點未處理的子節(jié)點數(shù)組中的第一個元素,對應(yīng)到vue源碼中的oldStartVnode

  • 舊后,指的是舊節(jié)點未處理的子節(jié)點數(shù)組中的最后一個元素,對應(yīng)到vue源碼中的oldEndVnode

子節(jié)點比較過程

接下來對上面的比較過程以及比較后做的操作做下說明

  • 新前與舊前的比較,如果他們相同,那么進行上面說到的patchVnode更新操作,然后新舊節(jié)點各向后一步,進行第二項的比較...直到遇到不同才會換種比較方式

怎樣深入理解vue中的虛擬DOM和Diff算法

if (sameVnode(oldStartVnode, newStartVnode)) {
  // 更新子節(jié)點
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  // 新舊各向后一步
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}
  • 新后與舊后的比較,如果他們相同,同樣進行pathchVnode更新,然后新舊各向前一步,進行前一項的比較...直到遇到不同,才會換比較方式

怎樣深入理解vue中的虛擬DOM和Diff算法

if (sameVnode(oldEndVnode, newEndVnode)) {
    //更新子節(jié)點
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    // 新舊向前
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
}
  • 新后與舊前的比較,如果它們相同,就進行更新操作,然后將舊前移動到所有未處理舊節(jié)點數(shù)組最后面,使舊前與新后位置保持一致,然后雙方一起向中間靠攏,新向前,舊向后。如果不同會繼續(xù)切換比較方式

怎樣深入理解vue中的虛擬DOM和Diff算法

if (sameVnode(oldStartVnode, newEndVnode)) {
  patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  //將舊子節(jié)點數(shù)組第一個子節(jié)點移動插入到最后
  canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  //舊向后
  oldStartVnode = oldCh[++oldStartIdx]
  //新向前
  newEndVnode = newCh[--newEndIdx]
  • 新前與舊后的比較,如果他們相同,就進行更新,然后將舊后移動到所有未處理舊節(jié)點數(shù)組最前面,是舊后與新前位置保持一致,,然后新向后,舊向前,繼續(xù)向中間靠攏。繼續(xù)比較剩余的節(jié)點。如果不同,就使用傳統(tǒng)的循環(huán)遍歷查找。

怎樣深入理解vue中的虛擬DOM和Diff算法

if (sameVnode(oldEndVnode, newStartVnode)) {
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  //將舊后移動插入到最前
  canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  //舊向前
  oldEndVnode = oldCh[--oldEndIdx]
  //新向后
  newStartVnode = newCh[++newStartIdx]
}
  • 循環(huán)遍歷查找,上面四種都沒找到的情況下,會通過key去查找匹配。

進行到這一步對于沒有設(shè)置key的節(jié)點,第一次會通過createKeyToOldIdx建立key與index的映射 {key:index}

// 對于沒有設(shè)置key的節(jié)點,第一次會通過createKeyToOldIdx建立key與index的映射 {key:index}
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

然后拿新節(jié)點的key與舊節(jié)點進行比較,找到key值匹配的節(jié)點的位置,這里需要注意的是,如果新節(jié)點也沒key,那么就會執(zhí)行findIdxInOld方法,從頭到尾遍歷匹配舊節(jié)點

//通過新節(jié)點的key,找到新節(jié)點在舊節(jié)點中所在的位置下標(biāo),如果沒有設(shè)置key,會執(zhí)行遍歷操作尋找
idxInOld = isDef(newStartVnode.key)
  ? oldKeyToIdx[newStartVnode.key]
  : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

//findIdxInOld方法
function findIdxInOld(node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i]
    //找到相同節(jié)點下標(biāo)
    if (isDef(c) && sameVnode(node, c)) return i
  }
}

如果通過上面的方法,依舊沒找到新節(jié)點與舊節(jié)點匹配的下標(biāo),那就說明這個節(jié)點是新節(jié)點,那就執(zhí)行新增的操作。

//如果新節(jié)點無法在舊節(jié)點中找到自己的位置下標(biāo),說明是新元素,執(zhí)行新增操作
if (isUndef(idxInOld)) {
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}

如果找到了,那么說明在舊節(jié)點中找到了key值一樣,或者節(jié)點和key都一樣的舊節(jié)點。如果節(jié)點一樣,那么在patchVnode之后,需要將舊節(jié)點移動到所有未處理節(jié)點之前,對于key一樣,元素不同的節(jié)點,將其認為是新節(jié)點,執(zhí)行新增操作。執(zhí)行完成后,新節(jié)點向后一步。

怎樣深入理解vue中的虛擬DOM和Diff算法

//如果新節(jié)點無法在舊節(jié)點中找到自己的位置下標(biāo),說明是新元素,執(zhí)行新增操作
if (isUndef(idxInOld)) {
  // 新增元素
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
  // 在舊節(jié)點中找到了key值一樣的節(jié)點
  vnodeToMove = oldCh[idxInOld]
  if (sameVnode(vnodeToMove, newStartVnode)) {
    // 相同子節(jié)點更新操作
    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    // 更新完將舊節(jié)點賦值undefined
    oldCh[idxInOld] = undefined
    //將舊節(jié)點移動到所有未處理節(jié)點之前
    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  } else {
    // 如果是相同的key,不同的元素,當(dāng)做新節(jié)點,執(zhí)行創(chuàng)建操作
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  }
}
//新節(jié)點向后
newStartVnode = newCh[++newStartIdx]

當(dāng)完成對舊節(jié)點的遍歷,但是新節(jié)點還沒完成遍歷,那就說明后續(xù)的都是新增節(jié)點,執(zhí)行新增操作,如果完成對新節(jié)點遍歷,舊節(jié)點還沒完成遍歷,那么說明舊節(jié)點出現(xiàn)冗余節(jié)點,執(zhí)行刪除操作。

//完成對舊節(jié)點的遍歷,但是新節(jié)點還沒完成遍歷,
if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  // 新增節(jié)點
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
  // 發(fā)現(xiàn)多余的舊節(jié)點,執(zhí)行刪除操作
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

子節(jié)點比較總結(jié)

上面就是子節(jié)點比較更新的一個完整過程,這是完整的邏輯代碼

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    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] //新后
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(newCh)
    }

    //雙端比較遍歷
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            //舊前向后移動
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
        } else if (isUndef(oldEndVnode)) {
            // 舊后向前移動
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            //新前與舊前
            //更新子節(jié)點
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                // 新舊各向后一步
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            //新后與舊后
            //更新子節(jié)點
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
                //新舊各向前一步
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // 新后與舊前
            //更新子節(jié)點
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
                //將舊前移動插入到最后
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
                //新向前,舊向后
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // 新前與舊后
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)

            //將舊后移動插入到最前
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)

            //新向后,舊向前
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            // 對于沒有設(shè)置key的節(jié)點,第一次會通過createKeyToOldIdx建立key與index的映射 {key:index}
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            //通過新節(jié)點的key,找到新節(jié)點在舊節(jié)點中所在的位置下標(biāo),如果沒有設(shè)置key,會執(zhí)行遍歷操作尋找
            idxInOld = isDef(newStartVnode.key) ?
                oldKeyToIdx[newStartVnode.key] :
                findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

            //如果新節(jié)點無法在舊節(jié)點中找到自己的位置下標(biāo),說明是新元素,執(zhí)行新增操作
            if (isUndef(idxInOld)) {
                // 新增元素
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                // 在舊節(jié)點中找到了key值一樣的節(jié)點
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    // 相同子節(jié)點更新操作
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                        // 更新完將舊節(jié)點賦值undefined
                    oldCh[idxInOld] = undefined
                        //將舊節(jié)點移動到所有未處理節(jié)點之前
                    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // 如果是相同的key,不同的元素,當(dāng)做新節(jié)點,執(zhí)行創(chuàng)建操作
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
                }
            }
            //新節(jié)點向后一步
            newStartVnode = newCh[++newStartIdx]
        }
    }

    //完成對舊節(jié)點的遍歷,但是新節(jié)點還沒完成遍歷,
    if (oldStartIdx > oldEndIdx) {
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
            // 新增節(jié)點
        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
        // 發(fā)現(xiàn)多余的舊節(jié)點,執(zhí)行刪除操作
        removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
}

關(guān)于怎樣深入理解vue中的虛擬DOM和Diff算法問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI