您好,登錄后才能下訂單哦!
這篇“Vue Diff算法怎么掌握”文章的知識點大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Vue Diff算法怎么掌握”文章吧。
對于一個容器(比如我們常用的#app)而言,它的內(nèi)容一般有三種情況:
字符串類型,即是文本。
子節(jié)點數(shù)組,即含有一個或者多個子節(jié)點
null,即沒有子節(jié)點
在vue中,會將dom元素當(dāng)作vdom進行處理,我們的HTML Attributes、事件綁定都會現(xiàn)在vdom上進行操作處理,最終渲染成真實dom。
Virtual Dom:用于描述真實dom節(jié)點的JavaScript對象。
使用vdom的原因在于,如果每次操作都是直接對真實dom進行操作,那么會造成很大的開銷。使用vdom時就能將性能消耗從真實dom操作的級別降低至JavaScript層面,相對而言更加優(yōu)秀。 一個簡單的vdom如下:
const vdom = {
type:"div",
props:{
class: "class",
onClick: () => { console.log("click") }
},
children: [] // 簡單理解這就是上述三種內(nèi)容
}
對于vue節(jié)點的更新而言,是采用的vdom進行比較。
diff算法便是用于容器內(nèi)容的第二種情況。當(dāng)更新前的容器中的內(nèi)容是一組子節(jié)點時,且更新后的內(nèi)容仍是一組節(jié)點。如果不采用diff算法,那么最簡單的操作就是將之前的dom全部卸載,再將當(dāng)前的新節(jié)點全部掛載。
但是直接操作dom對象是非常耗費性能的,所以diff算法的作用就是找出兩組vdom節(jié)點之間的差異,并盡可能的復(fù)用dom節(jié)點,使得能用最小的性能消耗完成更新操作。
接下來說三個diff算法,從簡單到復(fù)雜循序漸進。
為什么需要key?
接下來通過兩種情況進行說明為什么需要key?
如果存在如下兩組新舊節(jié)點數(shù)組:
const oldChildren = [
{type: 'p'},
{type: 'span'},
{type: 'div'},
]
const newChildren = [
{type: 'div'},
{type: 'p'},
{type: 'span'},
{type: 'footer'},
]
如果我們是進行正常的比較,步驟應(yīng)該是這樣:
找到相對而言較短的一組進行循環(huán)對比
第一個p標(biāo)簽與div標(biāo)簽不符,需要先將p標(biāo)簽卸載,再將div標(biāo)簽掛載。
第一個spam標(biāo)簽與p標(biāo)簽不符,需要先將span標(biāo)簽卸載,再將p標(biāo)簽掛載。
第一個div標(biāo)簽與span標(biāo)簽不符,需要先將div標(biāo)簽卸載,再將span標(biāo)簽掛載。
最后多余一個標(biāo)簽footer存在在新節(jié)點數(shù)組中,將其掛載即可。
那么我們發(fā)現(xiàn)其中進行了7次dom操作,但是命名前三個都是可以復(fù)用的,只是位置發(fā)生了變化。如果進行復(fù)用節(jié)點我們需要判斷兩個節(jié)點是相等的,但是現(xiàn)在的已有條件還不能滿足。
所以我們需要引入key,它相當(dāng)于是虛擬節(jié)點的身份證號,只要兩個虛擬節(jié)點的type和key都相同,我們便認為他們是相等的,可以進行dom的復(fù)用。
這時我們便可以找到復(fù)用的元素進行dom的移動,相對而言會比不斷的執(zhí)行節(jié)點的掛載卸載要好。
但是,dom的復(fù)用不意味不需要更新:
const oldVNode = {type: 'p', children: 'old', key: 1}
const newVNode = {type: 'p', children: 'new', key: 2}
上述節(jié)點擁有相同的type和key,我們可以復(fù)用,此時進行子節(jié)點的更新即可。
簡單的diff算法步驟
先用一個例子說明整個流程,再敘述其方法
const oldChildren = [
{type: 'p', children: 'p', key: 1},
{type: 'span', children: 'span', key: 2},
{type: 'div', children: 'div', key: 3},
{type: 'section', children: 'section', key : 4},
]
const newChildren = [
{type: 'div', children: 'new div', key: 3},
{type: 'p', children: 'p', key: 1},
{type: 'span', children: 'span', key: 2},
{type: 'footer', children: 'footer', key: 5},
]
為了敘述簡單,這里使用不同的標(biāo)簽。整個流程如下:
從新節(jié)點數(shù)組開始遍歷
第一個是div標(biāo)簽,當(dāng)前的下標(biāo)是0,之前的下標(biāo)是2。相對位置并未改變,不需要移動,只需要就行更新節(jié)點內(nèi)容即可。
第二個是p標(biāo)簽,當(dāng)前的下標(biāo)是1,之前的下標(biāo)是0。就相對位置而言,p相對于div標(biāo)簽有變化,需要進行移動。移動的位置就是在div標(biāo)簽之后。
第三個是span標(biāo)簽,當(dāng)前的下標(biāo)是2,之前的下標(biāo)是1。就相對位置而言,p相對于div標(biāo)簽有變化,需要進行移動。移動的位置就是在p標(biāo)簽之后。
第四個標(biāo)簽是footer,遍歷舊節(jié)點數(shù)組發(fā)現(xiàn)并無匹配的元素。代表當(dāng)前的元素是新節(jié)點,將其插入,插入的位置是span標(biāo)簽之后。
最后一步,遍歷舊節(jié)點數(shù)組,并去新節(jié)點數(shù)組中查找是否有對應(yīng)的節(jié)點,沒有則卸載當(dāng)前的元素。
如何找到需要移動的元素?
上述聲明了一個lastIdx變量,其初始值為0。作用是保存在新節(jié)點數(shù)組中,對于已經(jīng)遍歷了的新節(jié)點在舊節(jié)點數(shù)組的最大的下標(biāo)。那么對于后續(xù)的新節(jié)點來說,只要它在舊節(jié)點數(shù)組中的下標(biāo)的值小于當(dāng)前的lastIdx,代表當(dāng)前的節(jié)點相對位置發(fā)生了改變,則需要移動,
舉個例子:div在舊節(jié)點數(shù)組中的位置為2,大于當(dāng)前的lastIdx,更新其值為2。對于span標(biāo)簽,它的舊節(jié)點數(shù)組位置為1,其值更小。又因為當(dāng)前在新節(jié)點數(shù)組中處于div標(biāo)簽之后,就是相對位置發(fā)生了變化,便需要移動。
當(dāng)然,lastIdx需要動態(tài)維護。
總結(jié)
簡單diff算法便是拿新節(jié)點數(shù)組中的節(jié)點去舊節(jié)點數(shù)組中查找,通過key來判斷是否可以復(fù)用。并記錄當(dāng)前的lastIdx,以此來判斷節(jié)點間的相對位置是否發(fā)生變化,如果變化,需要進行移動。
簡單diff算法并不是最優(yōu)秀的,它是通過雙重循環(huán)來遍歷找到相同key的節(jié)點。舉個例子:
const oldChildren = [
{type: 'p', children: 'p', key: 1},
{type: 'span', children: 'span', key: 2},
{type: 'div', children: 'div', key: 3},
]
const newChildren = [
{type: 'div', children: 'new div', key: 3},
{type: 'p', children: 'p', key: 1},
{type: 'span', children: 'span', key: 2},
]
其實不難發(fā)現(xiàn),我們只需要將div標(biāo)簽節(jié)點移動即可,即進行一次移動。不需要重復(fù)移動前兩個標(biāo)簽也就是p、span標(biāo)簽。而簡單diff算法的比較策略即是從頭至尾的循環(huán)比較策略,具有一定的缺陷。
顧名思義,雙端diff算法是一種同時對新舊兩組子節(jié)點的兩個端點進行比較的算法
那么雙端diff算法開始的步驟如下:
比較 oldStartIdx節(jié)點 與 newStartIdx 節(jié)點,相同則復(fù)用并更新,否則
比較 oldEndIdx節(jié)點 與 newEndIdx 節(jié)點,相同則復(fù)用并更新,否則
比較 oldStartIdx節(jié)點 與 newEndIdx 節(jié)點,相同則復(fù)用并更新,否則
比較 oldEndIdx節(jié)點 與 newStartIdx 節(jié)點,相同則復(fù)用并更新,否則
簡單概括:
舊頭 === 新頭?復(fù)用,不需移動
舊尾 === 新尾?復(fù)用,不需移動
舊頭 === 新尾?復(fù)用,需要移動
舊尾 === 新頭?復(fù)用,需要移動
對于上述例子而言,比較步驟如下:
上述的情況是一種非常理想的情況,我們可以根據(jù)現(xiàn)有的diff算法完全的處理兩組節(jié)點,因為每一輪的雙端比較都會命中其中一種情況使得其可以完成處理。
但往往會有其他的情況,比如下面這個例子:
const oldChildren = [
{type: 'p', children: 'p', key: 1},
{type: 'span', children: 'span', key: 2},
{type: 'div', children: 'div', key: 3},
{type: 'ul', children: 'ul', key: 4},
]
const newChildren = [
{type: 'div', children: 'new div', key: 3},
{type: 'p', children: 'p', key: 1},
{type: 'ul', children: 'ul', key: 4},
{type: 'span', children: 'span', key: 2},
]
此時我們會發(fā)現(xiàn),上述的四個步驟都會無法命中任意一步。所以需要額外的步驟進行處理。即是:在四步比較失敗后,找到新頭節(jié)點在舊節(jié)點中的位置,并進行移動即可。動圖示意如下:
當(dāng)然還有刪除、增加等均不滿足上述例子的操作,但操作核心一致,這里便不再贅述。
總結(jié)
雙端diff算法的優(yōu)勢在于對于一些比較特殊的情況能更快的對節(jié)點進行處理,也更貼合實際開發(fā)。而雙端的含義便在于通過兩組子節(jié)點的頭尾分別進行比較并更新。
首先,快速diff算法包含了預(yù)處理步驟。它借鑒了純文本diff的思路,這時它為何快的原因之一。
比如:
const text1 = '我是快速diff算法'
const text2 = '我是雙端diff算法'
那么就會先從頭比較并去除可用元素,其次會重后比較相同元素并復(fù)用,那么結(jié)果就會如下:
const text1 = '快速'
const text2 = '雙端'
此時再進行一些其他的比較和處理便會簡單很多。
其次,快速diff算法還使用了一種算法來盡可能的復(fù)用dom節(jié)點,這個便是最長遞增子序列算法。為什么要用呢?先舉個例子:
// oldVNodes
const vnodes1 = [
{type:'p', children: 'p1', key: 1},
{type:'div', children: 'div', key: 2},
{type:'span', children: 'span', key: 3},
{type:'input', children: 'input', key: 4},
{type:'a', children: 'a', key: 6}
{type:'p', children: 'p2', key: 5},
]
// newVNodes
const vnodes2 = [
{type:'p', children: 'p1', key: 1},
{type:'span', children: 'span', key: 3},
{type:'div', children: 'div', key: 2},
{type:'input', children: 'input', key: 4},
{type:'p', children: 'p2', key: 5},
]
經(jīng)過預(yù)處理步驟之后得到的節(jié)點如下:
// oldVNodes
const vnodes1 = [
{type:'div', children: 'div', key: 2},
{type:'span', children: 'span', key: 3},
{type:'input', children: 'input', key: 4},
{type:'a', children: 'a', key: 6},
]
// newVNodes
const vnodes2 = [
{type:'span', children: 'span', key: 3},
{type:'div', children: 'div', key: 2},
{type:'input', children: 'input', key: 4},
]
此時我們需要獲得newVNodes節(jié)點相對應(yīng)oldVNodes節(jié)點中的下標(biāo)位置,我們可以采用一個source數(shù)組,先循環(huán)遍歷一次newVNodes,得到他們的key,再循環(huán)遍歷一次oldVNodes,獲取對應(yīng)的下標(biāo)關(guān)系,如下:
const source = new Array(restArr.length).fill(-1)
// 處理后
source = [1, 2, 0, -1]
注意!這里的下標(biāo)并不是完全正確!因為這是預(yù)處理后的下標(biāo),并不是剛開始的對應(yīng)的下標(biāo)值。此處僅是方便講解。 其次,source數(shù)組的長度是剩余的newVNodes的長度,若在處理完之后它的值仍然是-1則說明當(dāng)前的key對應(yīng)的節(jié)點在舊節(jié)點數(shù)組中沒有,即是新增的節(jié)點。
此時我們便可以通過source求得最長的遞增子序列的值為 [1, 2] 。對于index為1,2的兩個節(jié)點來說,他們的相對位置在原oldVNodes中是沒有變化的,那么便不需要移動他們,只需要移動其余的元素。這樣便能達到最大復(fù)用dom的效果。
步驟
以上述例子來說:
首先進行預(yù)處理
注意!預(yù)處理過的節(jié)點雖然復(fù)用,但仍然需要進行更新。
進行source填充
當(dāng)然這里遞增子序列 [1, 2] 和 [0, 1]都是可以的。
進行節(jié)點移動
用索引i指向新節(jié)點數(shù)組中的最后一個元素
用索引s指向最長遞增子序列中的最后一個元素
然后循環(huán)進行以下步驟比較:
source[i] === -1?等于代表新節(jié)點,掛載即可。隨后移動i
i === 遞增數(shù)組[s]? 等于代表當(dāng)前的節(jié)點存在在遞增子序列中,是復(fù)用的節(jié)點,當(dāng)前的節(jié)點無需移動。
上述均不成立代表需要移動節(jié)點。
節(jié)點更新,結(jié)束。
以上就是關(guān)于“Vue Diff算法怎么掌握”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。