通过几个问题深入分析Vue中的diff原理
遇到的問題
在使用Vue渲染“可刪減”的列表時,錯誤的使用index作為key,導(dǎo)致列表視圖出現(xiàn)錯亂。
點擊查看問題
- 復(fù)現(xiàn)步驟:右側(cè)有兩行,在第一行的Input里輸入1,在第二行Input里輸入2,然后點第一行的“ד刪除第一行
- 期待結(jié)果:刪除第一行后,應(yīng)該變成“請輸入 dog 的個數(shù):2”
- 實際結(jié)果:刪除第一行后,變成了“請輸入 dog 的個數(shù):1”
為什么cat變成了dog,但是<input />里的1沒有變成2呢?
這個問題一下子很難解釋,下面我們通過幾個小問題,一步一步來分析。
如果我們使用正確的值做為key,那么這個問題其實根本就沒有意義。但是,如果我們參透了其中的出錯原因,這將給我們帶來極大的提升。
為什么會觸發(fā)組件update
查看使用index作為key例子
- 測試1:打開瀏覽器控制臺,然后刪除第一行,查看日志,思考為什么。
- 測試2:先重置頁面,然后刪除最后一行,查看日志,思考為什么。
測試1的結(jié)果
你會發(fā)現(xiàn),刪除第一行后,watch和updated鉤子都執(zhí)行了,這個結(jié)果其實給了我們第一個提示:
刪除第一行這句話本質(zhì)上其實是:刪除vue實例數(shù)據(jù)中l(wèi)ist的第一項,并不是刪除dom的第一個節(jié)點!
對于在dom中的這三個節(jié)點,其實是做了如下的變化:
VDOM的diff算法
之所以會這種方式進行dom更新,這決定于vdom的diff算法,我們通過閱讀vue源碼中src/core/vdom/patch.js這個文件來一探究竟。具體的來說,就是其中的updateChildren方法:
不要被這么多變量嚇到,其實主要是三組變量,每組四個:
- 第一組是四個指針,分別指向oldCh和newCh的頭和尾
- 第二組是四個vnode,分別是四個指針?biāo)傅墓?jié)點
- 第三組是四個輔助變量(413行),用來移動vnode
在我們的例子里,大概是這樣:
繼續(xù)往下看:
又是一坨代碼,但也不要被嚇到,你會發(fā)現(xiàn)if、else if里的邏輯都是差不多的,仔細讀兩遍,你就會發(fā)現(xiàn)其實大概就是:
空節(jié)點跳過處理
指針1對應(yīng)的vnode跟指針3對應(yīng)的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走
指針2對應(yīng)的vnode跟指針4對應(yīng)的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走
指針1對應(yīng)的vnode跟指針4對應(yīng)的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走
指針2對應(yīng)的vnode跟指針3對應(yīng)的node比,如果是same的就patchVnode進行更新,如果不是same的就往下走
最終,如果到了這一步還不是same的,那就用key最終確認(rèn)一次
當(dāng)while循環(huán)退出時,如果指針1和指針2還沒重合,那就代表此時指針1和指針2區(qū)域內(nèi)的vnode是待刪除的,所以直接removeVnodes。而如果是指針3和指針4還沒重合,那就代表指針3和指針4之間的vnode是待添加的,所以直接addVnodes。至此整個過程結(jié)束。
怎么用上面的過程解釋“測試1”的結(jié)果
再看一下這個圖:
按照上面的diff算法,我們會先判斷cat和dog是否是same的,其中sameNode方法如下:
也就是說,只要key相同,并且tag、isComment、isDef(data)、sameInputType都是true,那么diff算法就認(rèn)為是同一個vnode,在這里舊的cat節(jié)點和新的dog節(jié)點,它們的key都是0,顯然符合這個條件。
所以,代碼會進入到這里:
此時會使用patchVnode方法來"patch"舊的這個cat節(jié)點,怎么patch呢?
簡單地說,就是使用新的props,讓這個cat節(jié)點進行re-render,re-render的過程中必然也做一些諸如:觸發(fā)watch,調(diào)用updated聲明周期鉤子之類的事情。 記住這句話,以后會用到!
接下來,dog變成pig,也是同樣的道理。
最后,左邊oldCh的pig節(jié)點哪去了呢?
其實到了這里,while循環(huán)就已經(jīng)退出了,看上一小節(jié)的第7步,此時pig節(jié)點會直接被remove掉。
關(guān)于patchVnode的細節(jié)在這里沒有寫,需要自己去看,關(guān)鍵的地方是在src/core/vdom/patch.js的545,552,572行
需要強調(diào)一點
可能有人會疑惑,即使我不知道diff算法的細節(jié),在我們刪除第一行時,也就是刪掉list的第一項時,會觸發(fā)視圖更新,視圖更新了,那cat節(jié)點肯定就會變成dog,這應(yīng)該是理所當(dāng)然的啊。
這里需要強調(diào)的是,使用diff算法時,"合適"的原有的節(jié)點是會被復(fù)用的!cat之所以變成dog,不是因為新建了一個dog節(jié)點,而是cat節(jié)點被復(fù)用,然后使用新的props,通過re-render實現(xiàn)了視圖的更新!
測試2為什么不觸發(fā)log的打印
到這里,我們就已經(jīng)解釋了:為什么“測試1”會觸發(fā)watch和updated的打印了。
那么為什么測試2不會觸發(fā)上述打印呢?其實原因很簡單,因為patchVnode提前return了,沒有觸發(fā)re-render:
回到最開始的問題
如下圖,到這里我們應(yīng)該已經(jīng)理解為什么刪除第一行后,cat會變成dog。但是,為什么<input />里的1沒有變成2呢?
一個簡單的解釋
我們之前說過:patchVnode的結(jié)果,其實就是使用新的props,讓這個cat節(jié)點進行re-render。
這里是re-render,它的執(zhí)行不是unmount一個節(jié)點,然后再mount一個新的節(jié)點,而是直接使用新的props來receive(更新)一個節(jié)點,節(jié)點的instance并沒有重置,所以re-render的過程中,data壓根就沒變。
receive這個詞出自:React實現(xiàn)原理
一些練手的問題 [可選]
使用空、常量1、index、unique的穩(wěn)定值、random的隨機值來作為key,依次預(yù)測視圖如何表現(xiàn)、控制臺如何打印:
練手問題
這樣就結(jié)束了嗎?
有一個更深層次的問題:這是一個feature還是一個bug?
我又用React寫了一個同樣的例子:點擊查看React版本的問題
你會發(fā)現(xiàn),不管是React還是Vue都會存在這個問題,這肯定不是一個bug,那么這兩個框架為什么要這么設(shè)計呢?
如果感興趣,請關(guān)注下一篇文章:《思考如何自己寫一個React框架》
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的通过几个问题深入分析Vue中的diff原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gcd的二进制优化笔记
- 下一篇: SpringBoot + AOP + M