Vue3快速diff算法的處理過程
一、為什么要使用diff算法
新舊vnode節(jié)點(diǎn)都有一組子節(jié)點(diǎn)的情況下,如果不使用diff算法處理則渲染器的做法是,將舊的子節(jié)點(diǎn)全部卸載,再掛載新的子節(jié)點(diǎn),并沒有考慮到節(jié)點(diǎn)的復(fù)用情況,比如下面的兩組vnode
const newVnode = { type: 'div', children: [ { type: 'p', children: '1' }, { type: 'p', children: '2' }, { type: 'p', children: '3' }, ] } const oldVnode = { type: 'div', children: [ { type: 'p', children: '4' }, { type: 'p', children: '5' }, { type: 'p', children: '6' } ] }
實(shí)際上并不需要去全部卸載然后掛載新的子節(jié)點(diǎn),只需要替換子節(jié)點(diǎn)中p
標(biāo)簽中的文本內(nèi)容即可。 Vue使用diff算法的原因就是為了避免全量更新子節(jié)點(diǎn),盡可能的去復(fù)用或者使用較少的操作去完成節(jié)點(diǎn)的更新。
二、如何復(fù)用子節(jié)點(diǎn)
1.判斷是否可復(fù)用:
觀察以下兩個(gè)新舊節(jié)點(diǎn):他們的類型相同都是p
元素,并且其內(nèi)容其實(shí)也沒有變化,只是元素的順序發(fā)生了變動(dòng),這種情況我們完全可以復(fù)用新舊節(jié)點(diǎn):
const newVnode = { type: 'div', children: [ { type: 'p', children: '1' }, { type: 'p', children: '2' }, { type: 'p', children: '3' }, ] } const oldVnode = { type: 'div', children: [ { type: 'p', children: '3' }, { type: 'p', children: '2' }, { type: 'p', children: '1' } ] }
為了能夠識(shí)別出哪些子節(jié)點(diǎn)是我們可以復(fù)用的,可以給其加上key屬性,當(dāng)新舊節(jié)點(diǎn)的key
值相同時(shí),則證明他們是同一個(gè)子節(jié)點(diǎn),可以復(fù)用。
const newVnode = { type: 'div', children: [ { type: 'p', children: '1', key:1 }, { type: 'p', children: '2', key:2 }, { type: 'p', children: '3', key:3 }, ] } const oldVnode = { type: 'div', children: [ { type: 'p', children: '3', key:3 }, { type: 'p', children: '2', key:2 }, { type: 'p', children: '1', key:1 }, ] }
2.對(duì)可復(fù)用節(jié)點(diǎn)的處理:
節(jié)點(diǎn)可復(fù)用并不意味著只需要簡(jiǎn)單的處理新舊子節(jié)點(diǎn)的順序變化,子節(jié)點(diǎn)的內(nèi)容可能也會(huì)發(fā)生變動(dòng),所以在移動(dòng)之前需要打補(bǔ)丁確保內(nèi)容更新:我們需要對(duì)前面處理子節(jié)點(diǎn)更新的patchChildren
進(jìn)行完善,主要處理其中新舊子節(jié)點(diǎn)都是多個(gè)的情況,此時(shí)我們才需要使用diff算法處理,其中再使用patch
函數(shù)去更新可復(fù)用節(jié)點(diǎn),具體的處理過程在下文中進(jìn)行描述:
function patchChildren(n1, n2, container) { if (typeof n2.children === 'string') { //省略代碼 } else if (Array.isArray(n2.children)) { //新子節(jié)點(diǎn)是一組節(jié)點(diǎn) if (Array.isArray(n1.children)) { //舊子節(jié)點(diǎn)也是一組節(jié)點(diǎn),應(yīng)用diff算法處理 //省略diff算法代碼 //diff中會(huì)使用patch去更新可復(fù)用元素 } else if (typeof n1.children === 'string') { //省略代碼 } } }
三、Vue3快速diff算法的處理過程
1.預(yù)處理:處理兩組子節(jié)點(diǎn)中首尾節(jié)點(diǎn)可復(fù)用的情況
比如下面的情況:
有三個(gè)節(jié)點(diǎn)key
值相同,可以復(fù)用,并且他們?cè)谧庸?jié)點(diǎn)中的相對(duì)順序也沒有發(fā)生變化,p-1
在最前面,p-2
和p-3
在最后面。所以他們并不需要移動(dòng),只需要處理中間的節(jié)點(diǎn)。
處理前置節(jié)點(diǎn):
設(shè)置一個(gè)索引j從0開始使用while循環(huán)尋找相同的前置節(jié)點(diǎn):如果是key相同的節(jié)點(diǎn),調(diào)用patch函數(shù)打補(bǔ)丁更新其中的內(nèi)容,直到使用同一個(gè)索引取到的新舊子節(jié)點(diǎn)key值不同
處理后置節(jié)點(diǎn):
拿到新舊子節(jié)點(diǎn)最后一個(gè)元素的索引oldEnd
和newEnd
,使用while從兩組節(jié)點(diǎn)尾部往上遍歷,如果是key相同的節(jié)點(diǎn)則調(diào)用patch
函數(shù)打補(bǔ)丁更新其中的內(nèi)容,知道取不到相同key的節(jié)點(diǎn)為止。
我們使用一個(gè)patchKeyedChildren
函數(shù)去實(shí)現(xiàn)上述過程:
function patchKeyedChildren(n1, n2, container) { const oldChildren = n1.children const newChildren = n2.children //處理前置節(jié)點(diǎn) let j = 0 let oldVNode = oldChildren[j] let newVNode = newChildren[j] while (oldVNode.key === newVNode.key) { patch(oldVNode, newVNode, container) j++ oldVNode = oldChildren[j] newVNode = newChildren[j] } //處理后置節(jié)點(diǎn) //將新舊節(jié)點(diǎn)的索引指向最后一個(gè)子節(jié)點(diǎn) let oldEnd = oldChildren.length - 1 let newEnd = newChildren.length - 1 oldVNode = oldChildren[oldEnd] newVNode = newChildren[newEnd] while (oldVNode.key === newVNode.key) { patch(oldVNode, newVNode, container) oldEnd-- newEnd-- oldVNode = oldChildren[oldEnd] newVNode = newChildren[newEnd] } }
2、預(yù)處理之后的兩種情況:需要?jiǎng)h除節(jié)點(diǎn)、需要新增節(jié)點(diǎn)
如何判斷存在需要?jiǎng)h除或者新增的節(jié)點(diǎn)? 在預(yù)處理之后我們可以獲得的信息有:
- 處理前置節(jié)點(diǎn)的時(shí)候獲得的索引
j
- 處理后置節(jié)點(diǎn)得到的兩個(gè)索引
newEnd
、oldEnd
利用以上索引可以做出判斷:
需要新增節(jié)點(diǎn)的情況:oldEnd < j 以及 newEnd >= j:
需要?jiǎng)h除節(jié)點(diǎn)的情況:oldEnd >= j 以及 newEnd < j:
在前文:
Vuejs 數(shù)據(jù)是如何渲染的?渲染器的簡(jiǎn)單實(shí)現(xiàn)
Vue 的渲染器是如何對(duì)節(jié)點(diǎn)進(jìn)行掛載和更新的中實(shí)現(xiàn)的patch和mountElement方法并不能指定位置去掛載節(jié)點(diǎn),為了能夠處理指定節(jié)點(diǎn)位置插入節(jié)點(diǎn),我們需要為其增加一個(gè)參數(shù)anchor,傳入錨點(diǎn)元素。
function patch(n1, n2, container, anchor) { //...省略代碼 if (typeof type === 'string') { if (!n1) { //在此處傳入錨點(diǎn)以支持新節(jié)點(diǎn)按位置插入 mountElement(n2, container, anchor) } else { patchElement(n1, n2) } } else if (type === Text) { //...省略代碼 } function mountElement(vnode, container, anchor) { //省略代碼 //給insert方法傳遞錨點(diǎn)元素 insert(el, container, anchor) } const renderer = createRenderer({ //...省略代碼 insert(el, parent, anchor = null) { //根據(jù)錨點(diǎn)元素插入節(jié)點(diǎn) parent.insertBefore(el, anchor) } })
接下來我們需要完善patchKeyedChildren去處理上述兩種情況:
需要新增節(jié)點(diǎn)時(shí):
function patchKeyedChildren(n1, n2, container) { const oldChildren = n1.children const newChildren = n2.children //需要插入新節(jié)點(diǎn) if (j > oldEnd && j <= newEnd){ //取得錨點(diǎn)索引 const anchorIndex = newEnd + 1 //取得錨點(diǎn)元素 const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null //調(diào)用patch掛載新節(jié)點(diǎn) while (j <= newEnd) { patch(null, newChildren[j++], container, anchor) } } }
代碼如上,我們首先使用newEnd+1
獲取錨點(diǎn)索引,并且使用newChildren[anchorIndex].el
去獲取到錨點(diǎn)元素,其中還做了一個(gè)判斷如果newEnd
是尾部節(jié)點(diǎn)那不需要提供錨點(diǎn)元素直接處理即可。
需要?jiǎng)h除節(jié)點(diǎn)時(shí):
function patchKeyedChildren(n1, n2, container) { const oldChildren = n1.children const newChildren = n2.children //需要插入新節(jié)點(diǎn) if (j > oldEnd && j <= newEnd){ //...省略新增節(jié)點(diǎn)邏輯 }else if (j > newEnd && j <= oldEnd) { //卸載節(jié)點(diǎn) while (j <= oldEnd) { unmount(oldChildren[j++]) } } }
如上所示,當(dāng)j<=oldEnd
時(shí)循環(huán)使用umount
卸載對(duì)應(yīng)的節(jié)點(diǎn)即可。
在實(shí)際過程中,很少會(huì)有像上述簡(jiǎn)單的預(yù)處理即可完成大部分工作的情況,這個(gè)時(shí)候就需要進(jìn)行進(jìn)一步的判斷: 比如以下情況:
在經(jīng)過預(yù)處理之后,只有首尾兩個(gè)節(jié)點(diǎn)被正確更新了,仍然會(huì)有多數(shù)節(jié)點(diǎn)沒有被更新。
預(yù)處理之后后續(xù)需要做的是:
- 判斷節(jié)點(diǎn)是否需要移動(dòng),移動(dòng)節(jié)點(diǎn);
- 如果有需要添加或者移除的節(jié)點(diǎn)進(jìn)行處理;
3.判斷節(jié)點(diǎn)是否需要移動(dòng):
1.構(gòu)建source數(shù)組
source數(shù)組需要去存儲(chǔ)新的子節(jié)點(diǎn)對(duì)應(yīng)的舊子節(jié)點(diǎn)的位置索引,然后去計(jì)算一個(gè)最長(zhǎng)遞增子序列,通過最長(zhǎng)遞增子序列去完成DOM的移動(dòng)操作
初始化source數(shù)組:
function patchKeyedChildren(n1, n2, container) { const oldChildren = n1.children const newChildren = n2.children //需要插入新節(jié)點(diǎn) if (j > oldEnd && j <= newEnd){ //...省略新增節(jié)點(diǎn)邏輯 }else if (j > newEnd && j <= oldEnd) { //卸載節(jié)點(diǎn) while (j <= oldEnd) { unmount(oldChildren[j++]) } //預(yù)處理完畢后 } else{ //初始化source數(shù)組 const count = newEnd - j + 1 const source = new Array(count) source.fill(-1) } }
source數(shù)組的長(zhǎng)度等于預(yù)處理之后剩余節(jié)點(diǎn)的長(zhǎng)度也就是newEnd - j + 1
,我們使用fill將數(shù)組中的元素填充為-1
初始化其中的值
填充source數(shù)組: 使用新子節(jié)點(diǎn)在舊子節(jié)點(diǎn)中的索引去填充source數(shù)組
如上key為p-3
的新子節(jié)點(diǎn)在舊子節(jié)點(diǎn)中的索引為2
,所以source數(shù)組的第一項(xiàng)需要被填充為2
,key
為p-4
的新子節(jié)點(diǎn)在舊子節(jié)點(diǎn)為3
,所以source數(shù)組的第二項(xiàng)的值為3
,以此類推。 在這個(gè)過程中需要嵌套兩個(gè)for循環(huán)去遍歷新舊子節(jié)類似下面的過程:
for (let i = oldStart; i <= oldEnd; i++) { const oldVNode = oldChildren[i] // 遍歷新的一組子節(jié)點(diǎn) for (let k = newStart; k <= newEnd; k++) { const newVNode = newChildren[k] // 找到擁有相同 key 值的可復(fù)用節(jié)點(diǎn) if (oldVNode.key === newVNode.key) { // 調(diào)用 patch 進(jìn)行更新 patch(oldVNode, newVNode, container) // 最后填充 source 數(shù)組 source[k - newStart] = i } } }
以上做法時(shí)間復(fù)雜度為O(n^2)
,在子節(jié)點(diǎn)數(shù)量增加時(shí)會(huì)存在性能問題。 優(yōu)化的辦法是先遍歷新的一組子節(jié)點(diǎn),根據(jù)子節(jié)點(diǎn)的位置和key生成一張索引表,然后再遍歷舊的一組子節(jié)點(diǎn),利用節(jié)點(diǎn)的key在索引表中找到對(duì)應(yīng)的新子節(jié)點(diǎn)的位置,以此填充source數(shù)組。
const oldStart = j const newStart = j const keyIndex = {} for(let i = newStart; i <= newEnd; i++) { keyIndex[newChildren[i].key] = i } for(let i = oldStart; i <= oldEnd; i++) { oldVNode = oldChildren[i] const k = keyIndex[oldVNode.key] if (typeof k !== 'undefined') { newVNode = newChildren[k] patch(oldVNode, newVNode, container) source[k - newStart] = i } else { unmount(oldVNode) } }
優(yōu)化后的代碼如上所示:
首先將預(yù)處理之后的j
值作為遍歷新舊節(jié)點(diǎn)開始時(shí)的索引,定義一個(gè)對(duì)象keyIndex
作為索引表,遍歷預(yù)處理之后剩余的一組新子節(jié)點(diǎn),將新子節(jié)點(diǎn)newChildren[i]
的key值與其位置索引放入索引表中。 遍歷舊子節(jié)點(diǎn),在遍歷時(shí),我們可以通過當(dāng)前節(jié)點(diǎn)的key
去keyIndex
索引表中獲取從而拿到當(dāng)前遍歷的舊子節(jié)點(diǎn)的oldChildren[i]
對(duì)應(yīng)的新節(jié)點(diǎn)的位置keyIndex[oldVNode.key]
,如果位置存在,說明節(jié)點(diǎn)可復(fù)用,使用patch
打補(bǔ)丁,并且使用當(dāng)前舊節(jié)點(diǎn)的索引i
對(duì)source數(shù)組進(jìn)行填充。
2.標(biāo)識(shí)是否需要移動(dòng)節(jié)點(diǎn)
需要添加標(biāo)識(shí)有:
- 是否需要移動(dòng)
moved
: 用于標(biāo)識(shí)是否有需要移動(dòng)的節(jié)點(diǎn), - 當(dāng)前新子節(jié)點(diǎn)的位置
pos
: 用于記錄遍歷舊子節(jié)點(diǎn)中遇到的最大的索引值k
,如果此次遍歷的k
值大于上一次的,說明相對(duì)位置正確無需移動(dòng), - 已經(jīng)更新過的節(jié)點(diǎn)數(shù)量
patched
:當(dāng)patched
大于source數(shù)組的長(zhǎng)度即newEnd - j + 1
時(shí)說明所有可復(fù)用節(jié)點(diǎn)已經(jīng)處理完畢,還有一些舊子節(jié)點(diǎn)需要執(zhí)行卸載操作, 代碼如下,我們?cè)诿恳淮胃鹿?jié)點(diǎn)內(nèi)容后遞增patched++
記錄處理數(shù)量,并對(duì)moved
和pos
的值進(jìn)行處理。
const count = newEnd - j + 1 // 新的一組子節(jié)點(diǎn)中剩余未處理節(jié)點(diǎn)的數(shù)量 const source = new Array(count) source.fill(-1) const oldStart = j const newStart = j let moved = false let pos = 0 const keyIndex = {} for(let i = newStart; i <= newEnd; i++) { keyIndex[newChildren[i].key] = i } let patched = 0 for(let i = oldStart; i <= oldEnd; i++) { oldVNode = oldChildren[i] if (patched < count) { const k = keyIndex[oldVNode.key] if (typeof k !== 'undefined') { newVNode = newChildren[k] patch(oldVNode, newVNode, container) patched++ source[k - newStart] = i // 判斷是否需要移動(dòng) if (k < pos) { moved = true } else { pos = k } } else { // 沒找到 unmount(oldVNode) } } else { unmount(oldVNode) } }
4.處理節(jié)點(diǎn)的移動(dòng):
先前我們使用moved
去標(biāo)記了是否有至少一個(gè)子節(jié)點(diǎn)需要移動(dòng),當(dāng)moved為true
時(shí),我們需要配合source數(shù)組中的最長(zhǎng)遞增子序列去移動(dòng)節(jié)點(diǎn),否則直接不用再去使用diff。
1.最長(zhǎng)遞增子序列:
什么是最長(zhǎng)遞增子序列 遞增子序列就是在一個(gè)序列中,從左到右依次找出更大的值所構(gòu)成的序列,在一個(gè)序列中可能存在多個(gè)遞增子序列,最長(zhǎng)遞增子序列就是其中長(zhǎng)度最長(zhǎng)的那個(gè)。 例如 在上面的例子中我們得到的source數(shù)組為[2, 3, 1, -1]
,則其最長(zhǎng)遞增子序列為[2,3]
,我們通過處理得到了對(duì)應(yīng)的舊子節(jié)點(diǎn)的索引[0, 1]
,即最長(zhǎng)遞增子序列對(duì)應(yīng)的新子節(jié)點(diǎn)的索引。
如上最長(zhǎng)遞增子序列對(duì)應(yīng)的舊節(jié)點(diǎn)為key為p-3
、p-4
,對(duì)應(yīng)在新子節(jié)點(diǎn)的位置為0
,1
。
最長(zhǎng)遞增子序列的意義:通過最長(zhǎng)遞增子序列得到的索引可以提示我們哪些元素的相對(duì)位置,在子節(jié)點(diǎn)更新后并未發(fā)生變化,我們可以保留這些節(jié)點(diǎn)的相對(duì)位置,然后去處理和移動(dòng)其他位置。如上p-3和p-4的相對(duì)位置在更新之后并未發(fā)生變化,即新節(jié)點(diǎn)中的索引為0
和1
的元素不需要移動(dòng)。這里我們省略求最長(zhǎng)遞增子序列的方法,直接將其當(dāng)作函數(shù)lis
處理source數(shù)組
的結(jié)果
const seq = lis(source)
2.根據(jù)最長(zhǎng)遞增子序列移動(dòng)節(jié)點(diǎn):
創(chuàng)建兩個(gè)索引輔助移動(dòng):
- 索引 i 指向新的一組子節(jié)點(diǎn)中的最后一個(gè)節(jié)點(diǎn)。
- 索引 s 指向最長(zhǎng)遞增子序列中的最后一個(gè)元素。
我們需要去判斷以下的情況:
source[i] === -1
: 節(jié)點(diǎn)不存在,需要掛載新節(jié)點(diǎn)i!==seq[s]
:節(jié)點(diǎn)需要移動(dòng),i===seq[s]
:節(jié)點(diǎn)無需移動(dòng),將s遞減并再次進(jìn)行比較
完善patchKeyedChildren
去處理這幾種情況:
function patchKeyedChildren(n1, n2, container) { //省略預(yù)處理和構(gòu)造source數(shù)組代碼 if (moved) { const seq = lis(source) // s 指向最長(zhǎng)遞增子序列的最后一個(gè)值 let s = seq.length - 1 let i = count - 1 for (i; i >= 0; i--) { if (source[i] === -1) { // 說明索引為 i 的節(jié)點(diǎn)是全新的節(jié)點(diǎn),應(yīng)該將其掛載 } else if (i !== seq[j]) { // 說明該節(jié)點(diǎn)需要移動(dòng) } else { // 當(dāng) i === seq[j] 時(shí),說明該位置的節(jié)點(diǎn)不需要移動(dòng) // 并讓 s 指向下一個(gè)位置 s-- } } } } }
節(jié)點(diǎn)不存在情況具體處理
if (source[i] === -1) { // 該節(jié)點(diǎn)在新的一組子節(jié)點(diǎn)中的真實(shí)位置索引 const pos = i + newStart const newVNode = newChildren[pos] // 該節(jié)點(diǎn)下一個(gè)節(jié)點(diǎn)的位置索引 const nextPos = pos + 1 // 錨點(diǎn) const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null patch(null, newVNode, container, anchor) }
代碼如上所示:當(dāng)新子節(jié)點(diǎn)是新節(jié)點(diǎn)時(shí)直接獲取,該節(jié)點(diǎn)的位置,即索引,并且加一獲得錨點(diǎn)用于掛載元素,如果元素本身就是最后一個(gè)元素 nextPos < newChildren.length
,則無需錨點(diǎn)。 此時(shí)p-7
處理完成,繼續(xù)向上處理p-2
節(jié)點(diǎn)需要移動(dòng)的情況
if (i !== seq[s]) { // 該節(jié)點(diǎn)在新的一組子節(jié)點(diǎn)中的真實(shí)位置索引 const pos = i + newStart const newVNode = newChildren[pos] // 該節(jié)點(diǎn)下一個(gè)節(jié)點(diǎn)的位置索引 const nextPos = pos + 1 // 錨點(diǎn) const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null patch(null, newVNode, container, anchor) }
邏輯和節(jié)點(diǎn)不存在的情況類似,只是移動(dòng)節(jié)點(diǎn)通過insert
函數(shù)去完成。此時(shí)處理的結(jié)果如下
節(jié)點(diǎn)不需要移動(dòng)的情況 對(duì)于p-3
和p-4
來說,source[i] !== -1
,并且i === seq[s]
,即節(jié)點(diǎn)無需移動(dòng)只需更新s
的值即可
s--
依此類推直到循環(huán)結(jié)束,子節(jié)點(diǎn)全部更新完畢,該過程完整代碼如下:
if (moved) { const seq = lis(source) // s 指向最長(zhǎng)遞增子序列的最后一個(gè)值 let s = seq.length - 1 let i = count - 1 for (i; i >= 0; i--) { if (source[i] === -1) { // 說明索引為 i 的節(jié)點(diǎn)是全新的節(jié)點(diǎn),應(yīng)該將其掛載 // 該節(jié)點(diǎn)在新 children 中的真實(shí)位置索引 const pos = i + newStart const newVNode = newChildren[pos] // 該節(jié)點(diǎn)下一個(gè)節(jié)點(diǎn)的位置索引 const nextPos = pos + 1 // 錨點(diǎn) const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null // 掛載 patch(null, newVNode, container, anchor) } else if (i !== seq[j]) { // 說明該節(jié)點(diǎn)需要移動(dòng) // 該節(jié)點(diǎn)在新的一組子節(jié)點(diǎn)中的真實(shí)位置索引 const pos = i + newStart const newVNode = newChildren[pos] // 該節(jié)點(diǎn)下一個(gè)節(jié)點(diǎn)的位置索引 const nextPos = pos + 1 // 錨點(diǎn) const anchor = nextPos < newChildren.length ? newChildren[nextPos].el : null // 移動(dòng) insert(newVNode.el, container, anchor) } else { // 當(dāng) i === seq[j] 時(shí),說明該位置的節(jié)點(diǎn)不需要移動(dòng) // 并讓 s 指向下一個(gè)位置 s-- } } } }
總結(jié):
使用
diff
算法的原因:- 傳統(tǒng)的 DOM 更新方法會(huì)在有新舊子節(jié)點(diǎn)時(shí)卸載舊節(jié)點(diǎn)并掛載新節(jié)點(diǎn),這種方法沒有考慮到節(jié)點(diǎn)的復(fù)用可能性。
diff
算法通過比較新舊節(jié)點(diǎn)的差異來復(fù)用節(jié)點(diǎn),從而優(yōu)化性能。
- 傳統(tǒng)的 DOM 更新方法會(huì)在有新舊子節(jié)點(diǎn)時(shí)卸載舊節(jié)點(diǎn)并掛載新節(jié)點(diǎn),這種方法沒有考慮到節(jié)點(diǎn)的復(fù)用可能性。
節(jié)點(diǎn)復(fù)用依據(jù):key:
- 節(jié)點(diǎn)復(fù)用是通過比較節(jié)點(diǎn)的
key
和類型
來實(shí)現(xiàn)的。相同的key
和類型
表明兩個(gè)節(jié)點(diǎn)可以被視為同一個(gè),從而復(fù)用以減少 DOM 操作。
- 節(jié)點(diǎn)復(fù)用是通過比較節(jié)點(diǎn)的
Vue 3 diff算法的過程:
- 預(yù)處理階段:處理首尾節(jié)點(diǎn),找出新舊兩種子節(jié)點(diǎn)中首尾可復(fù)用的節(jié)點(diǎn)并更新。
- 處理理想情況下新增和刪除節(jié)點(diǎn):若通過預(yù)處理有一組節(jié)點(diǎn)已經(jīng)更新完畢,證明新的一組子節(jié)點(diǎn)只需新增或刪除部分節(jié)點(diǎn)即可完成更新。
- 構(gòu)造source數(shù)組:通過遍歷新舊兩組子節(jié)點(diǎn),構(gòu)造一個(gè)source數(shù)組,去存儲(chǔ)新的子節(jié)點(diǎn)對(duì)應(yīng)的舊子節(jié)點(diǎn)的位置索引,并在此過程中判斷是否需要使用diff算法處理移動(dòng)。
- 節(jié)點(diǎn)位置移動(dòng):根據(jù)最長(zhǎng)遞增子序列判斷具體的某個(gè)節(jié)點(diǎn)是否需要新增或者移動(dòng),在需要時(shí)移動(dòng)節(jié)點(diǎn)以匹配新的子節(jié)點(diǎn)順序。
diff算法帶來的效率提升:
- 算法避免了全量的 DOM 更新,通過巧妙的方法判斷哪些節(jié)點(diǎn)需要更新、移動(dòng)或重新掛載,從而降低了全量更新的成本和時(shí)間。
以上就是Vue3快速diff算法的處理過程的詳細(xì)內(nèi)容,更多關(guān)于Vue3快速diff算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue3中關(guān)于路由hash與History的設(shè)置
這篇文章主要介紹了vue3中關(guān)于路由hash與History的設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08解決VUEX的mapState/...mapState等取值問題
這篇文章主要介紹了解決VUEX的mapState/...mapState等取值問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-07-07vue利用全局導(dǎo)航守衛(wèi)作登錄后跳轉(zhuǎn)到未登錄前指定頁面的實(shí)例代碼
這篇文章主要介紹了vue利用全局導(dǎo)航守衛(wèi)作登錄后跳轉(zhuǎn)到未登錄前指定頁面,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-05-05vue3+ts+vite2項(xiàng)目實(shí)戰(zhàn)踩坑記錄
最近嘗試上手Vue3+TS+Vite,對(duì)比起Vue2有些不適應(yīng),但還是真香,下面這篇文章主要給大家介紹了關(guān)于vue3+ts+vite2項(xiàng)目的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09Vue-Router如何動(dòng)態(tài)更改當(dāng)前頁url query
這篇文章主要介紹了Vue-Router如何動(dòng)態(tài)更改當(dāng)前頁url query問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08vue跳轉(zhuǎn)同一個(gè)路由參數(shù)不同的問題
這篇文章主要介紹了vue跳轉(zhuǎn)同一個(gè)路由參數(shù)不同的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08npm install報(bào)錯(cuò)缺少python問題及解決
這篇文章主要介紹了npm install報(bào)錯(cuò)缺少python問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06