通过几个问题深入分析Vue中的diff原理
遇到的問題
在使用Vue渲染“可刪減”的列表時,錯誤的使用index作為key,導致列表視圖出現錯亂。
點擊查看問題
- 復現步驟:右側有兩行,在第一行的Input里輸入1,在第二行Input里輸入2,然后點第一行的“ד刪除第一行
- 期待結果:刪除第一行后,應該變成“請輸入 dog 的個數:2”
- 實際結果:刪除第一行后,變成了“請輸入 dog 的個數:1”
為什么cat變成了dog,但是<input />里的1沒有變成2呢?
這個問題一下子很難解釋,下面我們通過幾個小問題,一步一步來分析。
如果我們使用正確的值做為key,那么這個問題其實根本就沒有意義。但是,如果我們參透了其中的出錯原因,這將給我們帶來極大的提升。
為什么會觸發組件update
查看使用index作為key例子
- 測試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最終確認一次
當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的545,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框架》
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的通过几个问题深入分析Vue中的diff原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gcd的二进制优化笔记
- 下一篇: Vuex 2.0 源码分析