1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        轉(zhuǎn):Vue 2 diff 算法解析

        共 19776字,需瀏覽 40分鐘

         ·

        2023-03-04 06:00

        作者:Karl_fang

        鏈接:https://juejin.cn/post/7204752219915747388

        來(lái)源:稀土掘金

        著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

        寫(xiě)在前面

        因?yàn)橹翱疵嬖囍辈ヒ步?jīng)常問(wèn)到 Diff 算法,然后作者本人用 Vue2 比較多,所以打算研究一下 Vue2 的 Diff 算法,其實(shí)很早就想學(xué)的,但是可能因?yàn)楦杏X(jué) Diff 算法比較深?yuàn)W,就一直拖著沒(méi)學(xué),但是最近在準(zhǔn)備面試,就想著遲早都要學(xué)的,趁這個(gè)機(jī)會(huì)把 Diff 算法搞懂吧 ??,作者就花了一天的時(shí)間研究了一下,可能沒(méi)有完全理解 Diff 算法的精髓,請(qǐng)各位見(jiàn)諒。

        ?? 這個(gè)其實(shí)算作者的學(xué)習(xí)筆記,而且作者水平有限,該文章僅代表作者個(gè)人觀點(diǎn),如果有錯(cuò)誤可以評(píng)論區(qū)指出來(lái),會(huì)不斷完善;同時(shí)本文很長(zhǎng),所以請(qǐng)讀者們有耐心地看完,看完后作者相信你們會(huì)對(duì) Diff 算法有更深的了解。本人覺(jué)得本文比目前網(wǎng)上講解 Diff 算法的大部分文章要更好,因?yàn)楸疚膹膯?wèn)題出發(fā),教會(huì)大家如何思考,而不是直接從答案出發(fā),就像讀答案一樣,這樣感覺(jué)沒(méi)什么意思,本文一步一步的引導(dǎo)大家去感受 Diff 算法的精妙,同時(shí)最后也做了一下小實(shí)驗(yàn),讓大家對(duì) Diff 算法有更加直觀的感受 ??。

        為什么要用 Diff 算法

        虛擬 DOM

        因?yàn)?Vue2 底層是用虛擬 DOM 來(lái)表示頁(yè)面結(jié)構(gòu)的,虛擬 DOM其實(shí)就是一個(gè)對(duì)象,如果想知道怎么生成的,其實(shí)大概流程就是:

        • ? 首先解析模板字符串,也就是 .vue 文件

        • ? 然后轉(zhuǎn)換成 AST 語(yǔ)法樹(shù)

        • ? 接著生成 render 函數(shù)

        • ? 最后調(diào)用 render 函數(shù),就能生成虛擬 DOM

        最小量更新

        其實(shí)框架為了性能才使用的虛擬 DOM,因?yàn)?js 生成 DOM 然后展示頁(yè)面是很消耗性能的,如果每一次的更新都把整個(gè)頁(yè)面重新生成一遍那體驗(yàn)肯定不好,所以需要找到兩個(gè)頁(yè)面中變化的地方,然后只要把變化的地方用 js 更新 (可能是增加、刪除或者更新) 一下就行了,也就是最小量更新。那么怎么實(shí)現(xiàn)最小量更新呢?那么就要用 Diff 算法了,那么 Diff 算法對(duì)比的到底是什么呢?可能這是剛學(xué) Diff 算法比較容易誤解的地方,其實(shí)比對(duì)的是新舊虛擬 DOM,所以 Diff 算法就是找不同,找到兩次虛擬 DOM 的不同之處,然后將不同反應(yīng)到頁(yè)面上,這就實(shí)現(xiàn)了最小量更新,如果只更新變化的地方那性能肯定更好。

        頁(yè)面更新流程

        其實(shí)這個(gè)比較難解釋?zhuān)髡咭簿痛笾抡f(shuō)一下,學(xué)了 Vue 的都知道這個(gè)框架的特點(diǎn)之一就有數(shù)據(jù)響應(yīng)式,什么是響應(yīng)式,也就是數(shù)據(jù)更新頁(yè)面也更新,那么頁(yè)面是怎么知道自己要更新了呢?其實(shí)這就是這個(gè)框架比較高明的地方了,大致流程如下:

        • ? 之前也說(shuō)了會(huì)運(yùn)行 render 函數(shù),那么運(yùn)行 render 函數(shù)的時(shí)候會(huì)被數(shù)據(jù)劫持,也就是進(jìn)入 Object.defineProperty 的 get,那么在這里收集依賴(lài),那么是誰(shuí)收集依賴(lài)呢?是每個(gè)組件,每個(gè)組件就是一個(gè) Watcher,會(huì)記錄這個(gè)組件內(nèi)的所有變量 _(也就是依賴(lài))_,當(dāng)然每個(gè)依賴(lài) (Dep) 也會(huì)收集自己所在組件的 Watcher;

        • ? 然后當(dāng)頁(yè)面中的數(shù)據(jù)發(fā)生變化,那么就會(huì)觸發(fā) Object.defineProperty 的 set,在這個(gè)函數(shù)里面數(shù)據(jù)就會(huì)通知每個(gè) Watcher 更新頁(yè)面,然后每個(gè)頁(yè)面就會(huì)調(diào)用更新方法,這個(gè)方法就是 patch;

        • ? 接著,就要找到兩個(gè)頁(yè)面之間的變化量,那么就要用到 Diff 算法了

        • ? 最后找到變化量后就可以進(jìn)行更新頁(yè)面了

        其實(shí)是邊找邊更新的,為了讓大家理解容易就將這兩個(gè)部分分開(kāi)了

        Diff 算法簡(jiǎn)單介紹

        面試問(wèn)到 Diff 算法是什么,大家肯定會(huì)說(shuō)兩句,比如 頭頭、尾尾、尾頭、頭尾、深度優(yōu)先遍歷(dfs)、同層比較 類(lèi)似這些話語(yǔ),雖然能說(shuō)一兩句其實(shí)也是淺嘗輒止。其實(shí)作者看了 CSDN 上發(fā)的關(guān)于 Diff 算法的文章,就是閱讀量很高的文章,作者覺(jué)得他也沒(méi)講明白,可能他自己沒(méi)明白,或者自己明白了但是沒(méi)講清楚,那么作者會(huì)用自己的感悟和大家分享一下。

        Diff 算法的前提

        為了讓大家能夠了解清楚,這里先說(shuō)明一下函數(shù)調(diào)用流程:

        • ? patch

        • ? patchVnode

        • ? updateChildren

        Diff 算法的 前提 這個(gè)是很重要的,可能大家會(huì)問(wèn)什么是前提?不就是之前說(shuō)的那些比較嘛?說(shuō)的沒(méi)錯(cuò)但也不對(duì),因?yàn)?Diff 算法到達(dá)之前說(shuō)的 頭頭、尾尾、尾頭、頭尾 這一步的前提就是兩次對(duì)比的節(jié)點(diǎn)是 相同的,這里的相同不是大家想的完全相同,只是符合某些條件就是相同了,為了簡(jiǎn)化說(shuō)明,文章就只考慮一個(gè)標(biāo)簽只包含 key 和 標(biāo)簽名(tag),那么之前說(shuō)的相同就是 key 相同以及 tag 相同,為了證明作者的說(shuō)法是有點(diǎn)正確的,那么這里也貼上源碼:

        // https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts
        // 36行
        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)))
          )
        }

        如果怕亂了,下面的可以省略不看也沒(méi)事不影響整體了解,下面只是為了考慮所有情況才加的一個(gè)判斷:那么如果兩個(gè)虛擬 DOM 不相同其實(shí)就不用繼續(xù)比較了,而且如果相同也不用比較了,這里的相同是真的完全相同,也就是兩個(gè)虛擬 DOM 的地址是一樣的,那么也貼上源碼:

        function patchVnode(......) {
          if (oldVnode === vnode) {
            return
          }
          ......
        }

        到目前為止大家可能會(huì)比較亂,現(xiàn)在總結(jié)一下:

        • ? 在 patch 函數(shù)里比較的是新老虛擬 DOM 是否是 key 相同以及 tag 相同,如果不相同那么就直接替換,如果相同用 patchVnode

        說(shuō)了這么多,其實(shí)作者也就想表達(dá)一個(gè)觀點(diǎn),就是只有當(dāng)兩次比較的虛擬 DOM 是 相同的 才會(huì)考慮 Diff 算法,如果不符合那直接把原來(lái)的刪除,替換新的 DOM 就行了。

        patchVnode 函數(shù)

        這個(gè)函數(shù)里的才是真正意義上的 Diff 算法,那么接下來(lái)會(huì)結(jié)合源碼向大家介紹一下。

        源碼中核心代碼在 patch.ts 的 638 行至 655 行。

        其實(shí),目前介紹 patchVnode 的都是直接對(duì)著源碼來(lái)介紹的,但是大家可能不清楚為啥要這么分類(lèi),那么作者在這里就讓大家明白為什么這么分類(lèi),首先在這里說(shuō)一個(gè)結(jié)論:

        • ? 就是 text 屬性和 children 屬性不可能同時(shí)存在,這個(gè)需要大家看模板解析源碼部分

        那么在對(duì)比新舊節(jié)點(diǎn)的情況下,主要比較的就是是否存在 text 和 children 的情況,那么會(huì)有如下九種情況

        情況老節(jié)點(diǎn) text老節(jié)點(diǎn) children新節(jié)點(diǎn) text新節(jié)點(diǎn) children
        1????
        2????
        3????
        4????
        5????
        6????
        7????
        8????
        9????

        按照上面的表格,因?yàn)槿绻鹿?jié)點(diǎn)有文本節(jié)點(diǎn),其實(shí)老節(jié)點(diǎn)不管是什么都會(huì)被替換掉,那么就可以按照 新節(jié)點(diǎn) text 是否存在來(lái)分類(lèi),其實(shí) Vue 源碼也是這么來(lái)分類(lèi)的:

        if (isUndef(vnode.text)) {
          // 新虛擬 DOM 有子節(jié)點(diǎn)
        } else if (oldVnode.text !== vnode.text) {
          // 如果新虛擬 DOM 是文本節(jié)點(diǎn),直接用 textContent 替換掉
          nodeOps.setTextContent(elm, vnode.text)
        }

        那么如果有子節(jié)點(diǎn)的話,那應(yīng)該怎么分類(lèi)呢?我們可以按照每種情況需要做什么來(lái)進(jìn)行分類(lèi),比如說(shuō):

        • ? 第一種情況,我們啥都不用做,因此也可以不用考慮

        • ? 第二種情況,我們應(yīng)該把原來(lái) DOM 的 textContent 設(shè)置為 ''

        • ? 第三種情況,我們也應(yīng)該把原來(lái) DOM 的 textContent 設(shè)置為 ''

        • ? 第四種情況,我們應(yīng)該加入新的子節(jié)點(diǎn)

        • ? 第五種情況,這個(gè)情況比較復(fù)雜,需要對(duì)比新老子節(jié)點(diǎn)的不同

        • ? 第六種情況,我們應(yīng)該把原來(lái)的 textContent 設(shè)置為 '' 后再加入新的子節(jié)點(diǎn)

        那么通過(guò)以上六種情況 _(新虛擬 DOM 不含有 text,也就是不是文本節(jié)點(diǎn)的情況)_,我們可以很容易地進(jìn)行歸類(lèi):

        • ? 分類(lèi) 1??: 第二種情況 和 第三種情況。進(jìn)行的操作是:把原來(lái) DOM 的 textContent 設(shè)置為 ''

        • ? 分類(lèi) 2??: 第四種情況 和 第六種情況。進(jìn)行的操作是:如果老虛擬 DOM 有 text,就置空,然后加入新的子節(jié)點(diǎn)

        • ? 分類(lèi) 3??:第五種情況。進(jìn)行的操作是:需要進(jìn)行精細(xì)比較,即對(duì)比新老子節(jié)點(diǎn)的不同

        其實(shí)源碼也是這么來(lái)進(jìn)行分類(lèi)的,而且之前說(shuō)的 同層比較 也就得出來(lái)了,因?yàn)槊看伪容^都是比較的同一個(gè)父節(jié)點(diǎn)每一個(gè)子元素 (這里的子元素包括文本節(jié)點(diǎn)和子節(jié)點(diǎn)) 是否相同,而 深度優(yōu)先遍歷(dfs) 是因?yàn)槊看伪容^中,如果該節(jié)點(diǎn)有子節(jié)點(diǎn) (這里的子節(jié)點(diǎn)指的是有 children 屬性,而不包括文本節(jié)點(diǎn)) 的話需要進(jìn)行遞歸遍歷,知道最后到文本節(jié)點(diǎn)結(jié)束。

        ?? 這里需要搞清楚子節(jié)點(diǎn)和子元素的區(qū)別和聯(lián)系

        然后我們來(lái)看看源碼是怎么寫(xiě)吧,只看新虛擬 DOM 不含有 text,也就是不是文本節(jié)點(diǎn)的情況:

        if (isUndef(vnode.text)) {
          if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch)
              // 遞歸處理,精細(xì)比較
              // 對(duì)應(yīng)分類(lèi) 3??
              updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
          } else if (isDef(ch)) {
            if (__DEV__) {
              checkDuplicateKeys(ch) // 可以忽略不看
            }
            // 對(duì)應(yīng)分類(lèi) 2??
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
          } else if (isDef(oldCh)) {
          // 對(duì)應(yīng)分類(lèi) 1??
            removeVnodes(oldCh, 0, oldCh.length - 1)
          } else if (isDef(oldVnode.text)) {
          // 對(duì)應(yīng)分類(lèi) 1??
            nodeOps.setTextContent(elm, '')
          }
        }

        ?我們可以看到源碼把分類(lèi) 1?? 拆開(kāi)來(lái)了,這是因?yàn)槿绻咸摂M DOM 有子節(jié),那么可能綁定了一些函數(shù),需要進(jìn)行解綁等一系列操作,作者也沒(méi)自信看,大致瞄了一眼,但是如果我們要求不高,如果只是想自己手動(dòng)實(shí)現(xiàn) Diff 算法,那么沒(méi)有拆開(kāi)的必要。

        作者覺(jué)得這么講可能比網(wǎng)上其他介紹 Diff 算法的要好,其他的可能直接給你說(shuō)源碼是怎么寫(xiě)的,可能沒(méi)有說(shuō)明白為啥這么寫(xiě),但是通過(guò)之前這么分析講解后可能你對(duì)為什么這么寫(xiě)會(huì)有更深的理解和幫助吧。

        updateChildren 函數(shù)

        同層比較

        因?yàn)楫?dāng)都含有子節(jié)點(diǎn),即都包含 children 屬性后,需要精細(xì)比較不同,不能像之前那些情況一樣進(jìn)行簡(jiǎn)單處理就可以了 那么這個(gè)函數(shù)中就會(huì)介紹大家經(jīng)常說(shuō)的 頭頭、尾尾、尾頭、頭尾 比較了,其實(shí)這不是 Vue 提出來(lái)的,是很早就提出來(lái)的算法,就一直沿用至今,大家可以參考【snabbdom 庫(kù)】

        ?? 在這之前我們要定義四個(gè)指針 newStartIdx、newEndIdx、oldStartIdx 和 oldEndIdx,分別指向 新頭節(jié)點(diǎn)、新尾節(jié)點(diǎn)、舊頭節(jié)點(diǎn) 與 舊尾節(jié)點(diǎn)

        循環(huán)條件如下:

        while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          ......
        }

        其實(shí)這個(gè)比較也就是按人類(lèi)的習(xí)慣來(lái)進(jìn)行比較的,比較順序如下 :

        • ? 1?? 新頭節(jié)點(diǎn)舊頭節(jié)點(diǎn)++newStartIdx 和 ++oldStartIdx

        • ? 2?? 新尾節(jié)點(diǎn)舊尾節(jié)點(diǎn)--newEndIdx 和 --oldEndIdx

        • ? 3?? 新尾節(jié)點(diǎn)舊頭節(jié)點(diǎn):需要將 舊頭節(jié)點(diǎn) 移動(dòng)到 舊尾節(jié)點(diǎn)之前,為什么要這么做,講起來(lái)比較復(fù)雜,記住就好,然后 --newEndIdx 和 ++oldStartIdx

        • ? 4?? 新頭節(jié)點(diǎn)舊尾節(jié)點(diǎn):需要將 舊尾節(jié)點(diǎn) 移動(dòng)到 舊頭節(jié)點(diǎn)之前,為什么要這么做,講起來(lái)比較復(fù)雜,記住就好,然后 ++newStartIdx 和 --oldEndIdx

        • ? 5?? 如果都沒(méi)有匹配的話,就把 新頭節(jié)點(diǎn) 在舊節(jié)點(diǎn)列表 (也就是 children 屬性的值) 中進(jìn)行查找,查找方式按照如下:

          • ? 找到了,那么只要將 新頭節(jié)點(diǎn) 添加到 舊頭節(jié)點(diǎn) 之前即可

          • ? 沒(méi)找到,那么需要?jiǎng)?chuàng)建新的元素然后添加到 舊頭節(jié)點(diǎn) 之前

          • ? 然后把這個(gè)節(jié)點(diǎn)設(shè)置為 undefined

          • ? 如果有 key 就把 key 在 oldKeyToIdx 進(jìn)行匹配,oldKeyToIdx 根據(jù)舊節(jié)點(diǎn)列表中元素的 key 生成對(duì)應(yīng)的下標(biāo)

          • ? 如果沒(méi)有,就按順序遍歷舊節(jié)點(diǎn)列表找到該節(jié)點(diǎn)所在的下標(biāo)

          • ? 如果在舊節(jié)點(diǎn)列表是否找到也分為兩種情況:

        根據(jù)循環(huán)條件我們可以得到兩種剩余情況,如下:

        • ? 6?? 如果 oldStartIdx > oldEndIdx 說(shuō)明老節(jié)點(diǎn)先遍歷完成,那么新節(jié)點(diǎn)比老節(jié)點(diǎn)多,就要把 newStartIdx 與 newEndIdx 之間的元素添加

        • ? 7?? 如果 newStartIdx > newEndIdx 說(shuō)明新節(jié)點(diǎn)先遍歷完成,那么老節(jié)點(diǎn)比新節(jié)點(diǎn)多,就要把 oldStartIdx 與 oldEndIdx 之間的元素刪除

        其實(shí)我們上面還沒(méi)有考慮如果節(jié)點(diǎn)為 undefined 的情況,因?yàn)樵谏厦嬉蔡岬竭^(guò),如果四種都不匹配后會(huì)將該節(jié)點(diǎn)置為 undefined,也只有舊節(jié)點(diǎn)列表中才有,因此要在開(kāi)頭考慮這兩種情況:

        • ? 8?? 當(dāng) oldStartVnode 為 undefined++oldStartIdx

        • ? 9?? 當(dāng) oldEndVnode 為 undefined--oldEndIdx

        那么我們來(lái)看源碼怎么寫(xiě)的吧,其中用到的函數(shù)可以查看源碼附錄

        // https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts
        // 439 行至 556 行
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          if (isUndef(oldStartVnode)) {
          // 情況 8??
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
          } else if (isUndef(oldEndVnode)) {
          // 情況 9??
            oldEndVnode = oldCh[--oldEndIdx]
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
          // 情況 1??
            patchVnode(...)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
          // 情況 2??
            patchVnode(...)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // Vnode moved right
            // 情況 3??
            patchVnode(...)
            canMove &&
              nodeOps.insertBefore(
                parentElm,
                oldStartVnode.elm,
                nodeOps.nextSibling(oldEndVnode.elm)
              )
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // Vnode moved left
            // 情況 4??
            patchVnode(...)
            canMove &&
              nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
          // 情況 5??
            if (isUndef(oldKeyToIdx)) // 創(chuàng)建 key -> index 的 Map
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) 
            // 找到 新頭節(jié)點(diǎn) 的下標(biāo)
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) {
              // New element
              // 如果沒(méi)找到
              createElm(...)
            } else {
              // 如果找到了
              vnodeToMove = oldCh[idxInOld]
              if (sameVnode(vnodeToMove, newStartVnode)) {
                patchVnode(...)
                oldCh[idxInOld] = undefined
                canMove &&
                  nodeOps.insertBefore(
                    parentElm,
                    vnodeToMove.elm,
                    oldStartVnode.elm
                  )
              } else {
                // same key but different element. treat as new element
                createElm(...)
              }
            }
            newStartVnode = newCh[++newStartIdx]
          }
        }
        if (oldStartIdx > oldEndIdx) {
          // 情況 6??
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(...)
        } else if (newStartIdx > newEndIdx) {
          // 情況 7??
          removeVnodes(...)
        }

        如果問(wèn)為什么這么比較,回答就是經(jīng)過(guò)很多人很多年的討論得出的,其實(shí)只要記住過(guò)程就行了,如果想要更深了解 Diff 算法,可以去 B 站看【尚硅谷】Vue源碼解析之虛擬DOM和diff算法

        v-for 中為什么要加 key

        這個(gè)問(wèn)題面試很常見(jiàn),但是可能大部分人也就只會(huì)背八股,沒(méi)有完全理解,那么經(jīng)過(guò)以上的介紹,我們可以得到自己的理解:

        • ? 首先,如果不加 key 的話,那么就不會(huì)去 Map 里匹配 _(O(1))_,而是循環(huán)遍歷整個(gè)列表 _(O(n))_,肯定加 key 要快一點(diǎn),性能更高

        • ? 其次,如果不加 key 那么在插入或刪除的時(shí)候就會(huì)出現(xiàn),原本不是同一個(gè)節(jié)點(diǎn)的元素被認(rèn)為是相同節(jié)點(diǎn),上面也有說(shuō)過(guò)是 sameVnode 函數(shù)判斷的,因此可能會(huì)有額外 DOM 操作

        為什么說(shuō)可能有額外 DOM 操作呢?這個(gè)和插入的地方有關(guān),之后會(huì)討論,同理刪除也一樣

        證明 key 的性能

        我們分為三個(gè)實(shí)驗(yàn):沒(méi)有 key、key 為 index、key 唯一,僅證明加了 key 可以進(jìn)行最小化更新操作。

        實(shí)驗(yàn)代碼

        有小伙伴評(píng)論說(shuō)可以把代碼貼上這樣更好,那么作者就把代碼附上 ??:

        <!DOCTYPE html>
        <html lang="en">

        <head>
          <meta charset="UTF-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Document</title>
          <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
          <style>
            .box {
              display: flex;
              flex-direction: row;
            }
            .item {
              flex: 1;
            }
          
        </style>
        </head>

        <body>
          <div id="app">
            <div class="box">
              <div class="item">
                <h3>沒(méi)有 key</h3>
                <p v-for="(item, index) in list">{{ item }}</p>
              </div>
              <div class="item">
                <h3>key 為 index</h3>
                <p v-for="(item, index) in list" :key="index">{{ item }}</p>
              </div>
              <div class="item">
                <h3>key 唯一</h3>
                <p v-for="(item, index) in list" :key="item">{{ item }}</p>
              </div>
            </div>
            <button @click="click1">push(4)</button>
            <button @click="click2">splice(1, 0, 666)</button>
            <button @click="click3">unshift(999)</button>
            <br /><br />
            <button @click="click4">pop()</button>
            <button @click="click5">splice(1, 1)</button>
            <button @click="click6">shift()</button>
          </div>
          <script>
            var app = new Vue({
              el: '#app',
              data: {
                show: false,
                list: [1, 2, 3],
              },
              methods: {
                click1() {
                  this.list.push(4);
                },
                click2() {
                  this.list.splice(1, 0, 666);
                },
                click3() {
                  this.list.unshift(999);
                },
                click4() {
                  this.list.pop();
                },
                click5() {
                  this.list.splice(1, 1);
                },
                click6() {
                  this.list.shift();
                }
              },
            })
          
        </script>
        </body>

        </html>
        復(fù)制代碼

        增加實(shí)驗(yàn)

        實(shí)驗(yàn)如下所示,我們首先更改原文字,然后點(diǎn)擊按鈕**「觀察節(jié)點(diǎn)發(fā)生變化的個(gè)數(shù)」**:

        在隊(duì)尾增加

        在隊(duì)內(nèi)增加

        在隊(duì)首增加


        刪除實(shí)驗(yàn)

        在隊(duì)尾刪除


        在隊(duì)內(nèi)刪除


        在隊(duì)首刪除


        實(shí)驗(yàn)結(jié)論

        增加實(shí)驗(yàn)

        表格為每次實(shí)驗(yàn)中,每種情況的最小更新量,假設(shè)列表原來(lái)的長(zhǎng)度為 n

        實(shí)驗(yàn)沒(méi)有 keykey 為 indexkey 唯一
        在隊(duì)尾增加111
        在隊(duì)中增加n - i + 1n - i + 11
        在隊(duì)首增加n + 1n + 11

        刪除實(shí)驗(yàn)

        表格為每次實(shí)驗(yàn)中,每種情況的最小更新量,假設(shè)列表原來(lái)的長(zhǎng)度為 n

        實(shí)驗(yàn)沒(méi)有 keykey 為 indexkey 唯一
        在隊(duì)尾刪除111
        在隊(duì)中刪除n - in - i1
        在隊(duì)首刪除nn1

        通過(guò)以上實(shí)驗(yàn)和表格可以得到加上 key 的性能和最小量更新的個(gè)數(shù)是最小的,雖然在 在隊(duì)尾增加 和 在隊(duì)尾刪除 的最小更新量相同,但是之前也說(shuō)了,如果沒(méi)有 key 是要循環(huán)整個(gè)列表查找的,時(shí)間復(fù)雜度是 O(n),而加了 key 的查找時(shí)間復(fù)雜度為 O(1),因此總體來(lái)說(shuō)加了 key 的性能要更好。

        寫(xiě)在最后

        本文從源碼和實(shí)驗(yàn)的角度介紹了 Diff 算法,相信大家對(duì) Diff 算法有了更深的了解了,如果有問(wèn)題可私信交流或者評(píng)論區(qū)交流,如果大家喜歡的話可以點(diǎn)贊 ? 收藏 ??

        源碼函數(shù)附錄

        列舉一些源碼中出現(xiàn)的簡(jiǎn)單函數(shù)

        setTextContent

        function setTextContent(node: Node, text: string) {
          node.textContent = text
        }

        isUndef

        function isUndef(v: any): v is undefined | null {
          return v === undefined || v === null
        }

        isDef

        function isDef<T>(v: T): v is NonNullable<T> {
          return v !== undefined && v !== null
        }

        insertBefore

        function insertBefore(
          parentNode: Node,
          newNode: Node,
          referenceNode: Node
        ) {
          parentNode.insertBefore(newNode, referenceNode)
        }

        nextSibling

        function nextSibling(node: Node) {
          return node.nextSibling
        }

        createKeyToOldIdx

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


        瀏覽 68
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            色色高清视频 | www.日韩一级 | 韩国毛片无码 | 美女撒尿 免费网站 | 成人欧美一区二区三区黑人3p | 激情五月丁香五月 | www国产亚洲精品 | 亚洲网站在线 | 91嫩草在线播放 | www.jiba |