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