溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue Diff算法怎么掌握

發(fā)布時間:2022-10-10 13:54:00 來源:億速云 閱讀:203 作者:iii 欄目:編程語言

這篇“Vue Diff算法怎么掌握”文章的知識點大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Vue Diff算法怎么掌握”文章吧。

為什么需要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ù)雜循序漸進。

簡單的Diff算法

為什么需要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)簽。整個流程如下:

Vue Diff算法怎么掌握

  • 從新節(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算法

簡單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é)點的兩個端點進行比較的算法

Vue Diff算法怎么掌握

那么雙端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ù)用,需要移動

對于上述例子而言,比較步驟如下:

Vue Diff算法怎么掌握

上述的情況是一種非常理想的情況,我們可以根據(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é)點中的位置,并進行移動即可。動圖示意如下:

Vue Diff算法怎么掌握

當(dāng)然還有刪除、增加等均不滿足上述例子的操作,但操作核心一致,這里便不再贅述。

總結(jié)

雙端diff算法的優(yōu)勢在于對于一些比較特殊的情況能更快的對節(jié)點進行處理,也更貼合實際開發(fā)。而雙端的含義便在于通過兩組子節(jié)點的頭尾分別進行比較并更新。

快速diff算法

首先,快速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ù)處理

Vue Diff算法怎么掌握

注意!預(yù)處理過的節(jié)點雖然復(fù)用,但仍然需要進行更新。

  • 進行source填充

Vue Diff算法怎么掌握

當(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é)點。

Vue Diff算法怎么掌握

節(jié)點更新,結(jié)束。

以上就是關(guān)于“Vue Diff算法怎么掌握”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對大家有幫助,若想了解更多相關(guān)的知識內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(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)容。

AI