通過幾個問題深入分析Vue中的key

XxjzZ發表於2019-04-10

遇到的問題

在使用Vue渲染“可刪減”的列表時,錯誤的使用index作為key,導致列表檢視出現錯亂。

點選檢視問題

  • 復現步驟:右側有兩行,在第一行的Input裡輸入1,在第二行Input裡輸入2,然後點第一行的“ד刪除第一行
  • 期待結果:刪除第一行後,應該變成“請輸入 dog 的個數:2”
  • 實際結果:刪除第一行後,變成了“請輸入 dog 的個數:1”

這個問題一下子很難解釋,下面我們通過幾個小問題,一步一步來分析。

如果我們使用正確的值做為key,那麼這個問題其實根本就沒有意義。但是,如果我們參透了其中的出錯原因,這將給我們帶來極大的提升。

為什麼會觸發元件update

檢視使用index作為key例子

  • 測試1: 開啟瀏覽器控制檯,然後刪除第一行,檢視日誌,思考為什麼
  • 測試2:先重置頁面,然後刪除最後一行,檢視日誌,思考為什麼

測試1的結果

你會發現,刪除第一行後,watchupdated鉤子都執行了,這個結果其實給了我們第一個提示:

刪除第一行這句話本質上其實是:刪除vue例項資料中list的第一項,並不是刪除dom的第一個節點!

對於在dom中的這三個節點,其實是做了如下的變化:

通過幾個問題深入分析Vue中的key

VDOM的diff演算法

之所以會這種方式進行dom更新,這決定於vdom的diff演算法,我們通過閱讀vue原始碼中src/core/vdom/patch.js這個檔案來一探究竟。具體的來說,就是其中的updateChildren方法:

通過幾個問題深入分析Vue中的key

不要被這麼多變數嚇到,其實主要是三組變數,每組四個:

  • 第一組是四個指標,分別指向oldCh和newCh的頭和尾
  • 第二組是四個vnode,分別是四個指標所指的節點
  • 第三組是四個輔助變數(413行),用來移動vnode

在我們的例子裡,大概是這樣:

通過幾個問題深入分析Vue中的key

繼續往下看:

通過幾個問題深入分析Vue中的key

又是一坨程式碼,但也不要被嚇到,你會發現if、else if裡的邏輯都是差不多的,仔細讀兩遍,你就會發現其實大概就是:

  1. 空節點跳過處理

  2. 指標1對應的vnode跟指標3對應的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走

  3. 指標2對應的vnode跟指標4對應的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走

  4. 指標1對應的vnode跟指標4對應的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走

  5. 指標2對應的vnode跟指標3對應的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走

  6. 最終,如果到了這一步還不是same的,那就用key最終確認一次

    1. 先構造一個key to index的map
    2. 判斷當前old vnode的key是否在map裡
    3. 如果不在,就直接createElm
    4. 如果在,並且是same的,那就patchVnode,然後更新節點內容、順序
    5. 如果在,但是不是same的,那就視作新element,執行createElm
  7. 當while迴圈退出時,如果指標1和指標2還沒重合,那就代表此時指標1和指標2區域內的vnode是待刪除的,所以直接removeVnodes。而如果是指標3和指標4還沒重合,那就代表指標3和指標4之間的vnode是待新增的,所以直接addVnodes。至此整個過程結束。

怎麼用上面的過程解釋“測試1”的結果

再看一下這個圖:

通過幾個問題深入分析Vue中的key

按照上面的diff演算法,我們會先判斷cat和dog是否是same的,其中sameNode方法如下:

通過幾個問題深入分析Vue中的key

也就是說,只要key相同,並且tag、isComment、isDef(data)、sameInputType都是true,那麼diff演算法就認為是同一個vnode,在這裡舊的cat節點和新的dog節點,它們的key都是0,顯然符合這個條件。

所以,程式碼會進入到這裡:

通過幾個問題深入分析Vue中的key

此時會使用patchVnode方法來"patch"舊的這個cat節點,怎麼patch呢?

簡單地說,就是使用新的props,讓這個cat節點進行re-render,re-render的過程中必然也做一些諸如:觸發watch,呼叫updated宣告週期鉤子之類的事情。 記住這句話,以後會用到!

接下來,dog變成pig,也是同樣的道理。

最後,左邊oldCh的pig節點哪去了呢?

其實到了這裡,while迴圈就已經退出了,看上一小節的第7步,此時pig節點會直接被remove掉。

關於patchVnode的細節在這裡沒有寫,需要自己去看,關鍵的地方是在src/core/vdom/patch.js的552行和572行

需要強調一點

可能有人會疑惑,即使我不知道diff演算法的細節,在我們刪除第一行時,也就是刪掉list的第一項時,會觸發檢視更新,檢視更新了,那cat節點肯定就會變成dog,這應該是理所當然的啊。

這裡需要強調的是,使用diff演算法時,"合適"的原有的節點是會被複用的!cat之所以變成dog,不是因為新建了一個dog節點,而是cat節點被複用,然後使用新的props,通過re-render實現了檢視的更新!

測試2為什麼不觸發log的列印

到這裡,我們就已經解釋了:為什麼“測試1”會觸發watchupdated的列印了。

那麼為什麼測試2不會觸發上述列印呢?其實原因很簡單,因為patchVnode提前return了,沒有觸發re-render:

通過幾個問題深入分析Vue中的key

回到最開始的問題

如下圖,到這裡我們應該已經理解為什麼刪除第一行後,cat會變成dog。但是,為什麼<input />裡的1沒有變成2呢?

通過幾個問題深入分析Vue中的key

一個簡單的解釋

我們之前說過:patchVnode的結果,其實就是使用新的props,讓這個cat節點進行re-render。

這裡是re-render,它的執行不是unmount一個節點,然後再mount一個新的節點,而是直接使用新的props來receive(更新)一個節點,節點的instance並沒有重置,所以re-render的過程中,data壓根就沒變。

receive這個詞出自:React實現原理

一些練手的問題 [可選]

使用空、常量1、index、unique的穩定值、random的隨機值來作為key,依次預測檢視如何表現、控制檯如何列印:

練手問題

這樣就結束了嗎?

有一個更深層次的問題:這是一個feature還是一個bug?

我又用React寫了一個同樣的例子:點選檢視React版本的問題

你會發現,不管是React還是Vue都會存在這個問題,這肯定不是一個bug,那麼這兩個框架為什麼要這麼設計呢?

如果感興趣,請關注下一篇文章:《思考如何自己寫一個React框架》

相關文章