遇到的問題
在使用Vue渲染“可刪減”的列表時,錯誤的使用index作為key,導致列表檢視出現錯亂。
- 復現步驟:右側有兩行,在第一行的Input裡輸入1,在第二行Input裡輸入2,然後點第一行的“ד刪除第一行
- 期待結果:刪除第一行後,應該變成“請輸入 dog 的個數:2”
- 實際結果:刪除第一行後,變成了“請輸入 dog 的個數:1”
這個問題一下子很難解釋,下面我們通過幾個小問題,一步一步來分析。
如果我們使用正確的值做為key,那麼這個問題其實根本就沒有意義。但是,如果我們參透了其中的出錯原因,這將給我們帶來極大的提升。
為什麼會觸發元件update
- 測試1: 開啟瀏覽器控制檯,然後刪除第一行,檢視日誌,思考為什麼
- 測試2:先重置頁面,然後刪除最後一行,檢視日誌,思考為什麼
測試1的結果
你會發現,刪除第一行後,watch
和updated鉤子
都執行了,這個結果其實給了我們第一個提示:
刪除第一行這句話本質上其實是:刪除vue例項資料中list的第一項,並不是刪除dom的第一個節點!
對於在dom中的這三個節點,其實是做了如下的變化:
VDOM的diff演算法
之所以會這種方式進行dom更新,這決定於vdom的diff演算法,我們通過閱讀vue原始碼中src/core/vdom/patch.js
這個檔案來一探究竟。具體的來說,就是其中的updateChildren
方法:
不要被這麼多變數嚇到,其實主要是三組變數,每組四個:
- 第一組是四個指標,分別指向oldCh和newCh的頭和尾
- 第二組是四個vnode,分別是四個指標所指的節點
- 第三組是四個輔助變數(413行),用來移動vnode
在我們的例子裡,大概是這樣:
繼續往下看:
又是一坨程式碼,但也不要被嚇到,你會發現if、else if裡的邏輯都是差不多的,仔細讀兩遍,你就會發現其實大概就是:
-
空節點跳過處理
-
指標1對應的vnode跟指標3對應的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走
-
指標2對應的vnode跟指標4對應的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走
-
指標1對應的vnode跟指標4對應的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走
-
指標2對應的vnode跟指標3對應的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走
-
最終,如果到了這一步還不是same的,那就用key最終確認一次
- 先構造一個key to index的map
- 判斷當前old vnode的key是否在map裡
- 如果不在,就直接createElm
- 如果在,並且是same的,那就patchVnode,然後更新節點內容、順序
- 如果在,但是不是same的,那就視作新element,執行createElm
-
當while迴圈退出時,如果指標1和指標2還沒重合,那就代表此時指標1和指標2區域內的vnode是待刪除的,所以直接removeVnodes。而如果是指標3和指標4還沒重合,那就代表指標3和指標4之間的vnode是待新增的,所以直接addVnodes。至此整個過程結束。
怎麼用上面的過程解釋“測試1”的結果
再看一下這個圖:
按照上面的diff演算法,我們會先判斷cat和dog是否是same的,其中sameNode方法如下:
也就是說,只要key相同,並且tag、isComment、isDef(data)、sameInputType都是true,那麼diff演算法就認為是同一個vnode,在這裡舊的cat節點和新的dog節點,它們的key都是0,顯然符合這個條件。
所以,程式碼會進入到這裡:
此時會使用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”會觸發watch
和updated
的列印了。
那麼為什麼測試2不會觸發上述列印呢?其實原因很簡單,因為patchVnode
提前return
了,沒有觸發re-render:
回到最開始的問題
如下圖,到這裡我們應該已經理解為什麼刪除第一行後,cat會變成dog。但是,為什麼<input />
裡的1沒有變成2呢?
一個簡單的解釋
我們之前說過: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框架》