一文詳解Vue3中簡單diff算法的實(shí)現(xiàn)
簡單Diff算法
核心Diff只關(guān)心新舊虛擬節(jié)點(diǎn)都存在一組子節(jié)點(diǎn)的情況
減少DOM操作
例子
// 舊節(jié)點(diǎn)
const oldVNode = {
type: 'div',
children: [
{ type: 'p', children: '1' },
{ type: 'p', children: '2' },
{ type: 'p', children: '3' }
]
}
// 新節(jié)點(diǎn)
const newVNode = {
type: 'div',
children: [
{ type: 'p', children: '4' },
{ type: 'p', children: '5' },
{ type: 'p', children: '6' }
]
}
如果直接去操作DOM,那么上面的更新需要6次DOM操作,卸載所有舊子節(jié)點(diǎn),掛載所有新子節(jié)點(diǎn)。
但是觀察上面新舊vNode的子節(jié)點(diǎn)可以發(fā)現(xiàn):
- 更新前后所有子節(jié)點(diǎn)都是 p 標(biāo)簽,即標(biāo)簽元素步變
- 只有p標(biāo)簽的子節(jié)點(diǎn)發(fā)生變化了
所以最理想的更新方式是直接更新這個(gè)p標(biāo)簽的文本節(jié)點(diǎn)的內(nèi)容,這樣只需要一次DOM操作,即可完成一個(gè)p標(biāo)簽的更新。更新完所有節(jié)點(diǎn)只需要3次DOM操作就可以完成全部節(jié)點(diǎn)的更新。
上面的做法可以減少DOM操作次數(shù),但問題也很明顯,只有節(jié)點(diǎn)數(shù)量相同這個(gè)做法才能正常工作。但新舊兩組子節(jié)點(diǎn)數(shù)量未必相同。

新的一組子節(jié)點(diǎn)數(shù)量少于舊的一組子節(jié)點(diǎn)的數(shù)量時(shí),意味著有節(jié)點(diǎn)在更新后應(yīng)該被卸載。(圖二)

新的一組子節(jié)點(diǎn)數(shù)量多余舊的一組子節(jié)點(diǎn)的數(shù)量時(shí),意味著有節(jié)點(diǎn)在更新后應(yīng)該被新增并掛載。(圖三)
結(jié)論
通過上面分析得出,進(jìn)行新舊兩組子節(jié)點(diǎn)的更新時(shí),不應(yīng)該總是遍歷舊的一組子節(jié)點(diǎn)或新的一組子節(jié)點(diǎn),而是應(yīng)該遍歷其中較短的一組。這樣才能盡可能多的調(diào)用patch進(jìn)行更新。接著對(duì)比新舊兩組子節(jié)點(diǎn)的長度,如果新的一組子節(jié)點(diǎn)更長,說明有新節(jié)點(diǎn)需要掛載,否則說明有舊的子節(jié)點(diǎn)需要卸載。
實(shí)現(xiàn)
function easyDiff (n1, n2, container) {
// 取出新舊子節(jié)點(diǎn)列表
const oldChildren = n1.children
const newChildren = n2.children
// 獲取新舊子節(jié)點(diǎn)列表的長度
const oldLen = oldChildren.length
const newLen = newChildren.length
// 取得較小的一個(gè)(可以理解為兩組子節(jié)點(diǎn)的公共長度)
const commonLength = Math.min(oldLen, newLen)
// 遍歷 commonLength 次
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i], container)
}
// 如果 newLen > oldLen,說明有新子節(jié)點(diǎn)需要掛載
if (newLen > oldLen) {
for (let i = commonLength; i < newLen; i++) {
patch(null, newChildren[i], container)
}
}
// 如果 oldLen > newLen,說明有舊節(jié)點(diǎn)需要卸載
if (oldLen > newLen) {
for (let i = commonLength; i < oldLen; i++) {
unmount(oldChildren[i])
}
}
}DOM復(fù)用與key的作用
例子
上面通過減少DOM操作次數(shù)提升了更新性能,但還存在可優(yōu)化空間
const KEY = {
oldVNode: [
{ type: 'p' },
{ type: 'div' },
{ type: 'span' }
],
newVNode: [
{ type: 'span' },
{ type: 'p' },
{ type: 'div' }
]
}
針對(duì)這個(gè)例子,如果還使用上面的算法,則需要6次DOM操作。
調(diào)用 patch 在 p標(biāo)簽和span標(biāo)簽之間打補(bǔ)丁,由于不是相同標(biāo)簽,所以p標(biāo)簽被卸載,然后掛載span標(biāo)簽,需要兩步操作,div - p,span - div同理。
很容易發(fā)現(xiàn)新舊兩組子節(jié)點(diǎn)只是順序不同。所以最優(yōu)的處理方式是,通過DOM的移動(dòng)來完成子節(jié)點(diǎn)的更新,這比不斷執(zhí)行卸載和掛載性能好得多。但是要通過移動(dòng)DOM來完成更新,必須要保證新舊兩組子節(jié)點(diǎn)的確存在可復(fù)用的節(jié)點(diǎn)。(如果新的子節(jié)點(diǎn)沒有在舊的子節(jié)點(diǎn)中出現(xiàn),則無法通過移動(dòng)節(jié)點(diǎn)的方式完成更新操作。)
用上面的例子來說,怎么確定新的一組節(jié)點(diǎn)中的第三個(gè)節(jié)點(diǎn) { type: 'div' } 與舊的一組子節(jié)點(diǎn)中的第二個(gè)節(jié)點(diǎn)相同呢?可以通過vNode.type判斷,但這種方式并不可靠。
oldChildren: [
{ type: 'p', children: '1' },
{ type: 'p', children: '2' },
{ type: 'p', children: '3' }
],
newChildren: [
{ type: 'p', children: '3' },
{ type: 'p', children: '1' },
{ type: 'p', children: '2' }
]觀察上面節(jié)點(diǎn),可以發(fā)現(xiàn),這個(gè)案例可以通過移動(dòng)DOM的方式來完成更新,但是vNode.type的值都相同,導(dǎo)致無法確定新舊節(jié)點(diǎn)中的對(duì)應(yīng)關(guān)系,就不能確定怎么移動(dòng)DOM完成更新。
虛擬節(jié)點(diǎn)的key
因此,需要引入額外的 key 作為vNode的標(biāo)識(shí)。
const KEY = {
oldChildren: [
{ type: 'p', children: '1', key: '1' },
{ type: 'p', children: '2', key: '2' },
{ type: 'p', children: '3', key: '3' }
],
newChildren: [
{ type: 'p', children: '3', key: '3' },
{ type: 'p', children: '1', key: '1' },
{ type: 'p', children: '2', key: '2' }
]
}key 屬性就像虛擬DOM的 身份證號(hào),只要兩個(gè)虛擬節(jié)點(diǎn)的type和key屬性都相同,那么就可以認(rèn)為它們是相同的;即可以進(jìn)行DOM的復(fù)用。

但是DOM可復(fù)用并不意味著不需要更新
oldVNode: { type: 'p', children: 'text - 1', key: '1' }
newVNode: { type: 'p', children: 'text - 2', key: '1' }
兩個(gè)節(jié)點(diǎn)有相同的key可type,但它們的文本內(nèi)容不同,還是需要通過patch進(jìn)行打補(bǔ)丁操作。
實(shí)現(xiàn)
function easyDiffV2 (n1, n2, container) {
// 取出新舊子節(jié)點(diǎn)列表
const oldChildren = n1.children
const newChildren = n2.children
// 遍歷新的children
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren.length; j++) {
const oldVNode = oldChildren[i]
// 如果找到可復(fù)用的兩個(gè)節(jié)點(diǎn)
if (newVNode.key === oldVNode.key) {
// 對(duì)可復(fù)用的兩個(gè)節(jié)點(diǎn)打補(bǔ)丁
patch(oldVNode, newVNode, container)
// 一個(gè)新節(jié)點(diǎn)處理完后開始下一個(gè)新節(jié)點(diǎn)
break
}
}
}
}外層循環(huán)遍歷新的一組子節(jié)點(diǎn),內(nèi)層循環(huán)遍歷舊的一組子節(jié)點(diǎn)。內(nèi)層循環(huán)中對(duì)比新舊子節(jié)點(diǎn)的key值,在舊的子節(jié)點(diǎn)中找到可以復(fù)用的節(jié)點(diǎn);一旦找到則調(diào)用 patch 打補(bǔ)丁。
找到需要移動(dòng)的元素
現(xiàn)在已經(jīng)可以通過key找到可復(fù)用的節(jié)點(diǎn)了,接下來要做的是判斷一個(gè)節(jié)點(diǎn)是否需要移動(dòng)
探索節(jié)點(diǎn)順序關(guān)系

節(jié)點(diǎn)順序不變 - 查找過程:
第一步:取新的一組子節(jié)點(diǎn)中的第一個(gè)節(jié)點(diǎn) p - 1,它的key為1,在舊的一組子節(jié)點(diǎn)中找到具有相同key值的可復(fù)用節(jié)點(diǎn),能夠找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中索引為0;p - 2、p = 3同理。
- key 為 1 的節(jié)點(diǎn)在 舊節(jié)點(diǎn)列表中的索引為0
- key 為 2 的節(jié)點(diǎn)在 舊節(jié)點(diǎn)列表中的索引為1
- key 為 3 的節(jié)點(diǎn)在 舊節(jié)點(diǎn)列表中的索引為2
每一次查找可復(fù)用節(jié)點(diǎn)都會(huì)記錄該可復(fù)用節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中的位置索引,如果按照先后順序排列,則可以得到一個(gè)序列:0、1、2,是一個(gè)遞增序列。

節(jié)點(diǎn)順序變化 - 查找過程
第一步:取新的一組子節(jié)點(diǎn)中的第一個(gè)節(jié)點(diǎn) p - 3,它的key為3,在舊的一組子節(jié)點(diǎn)中找到具有相同key值的可復(fù)用節(jié)點(diǎn),能夠找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中索引為2;
第二步:取新的一組子節(jié)點(diǎn)中的第一個(gè)節(jié)點(diǎn) p - 1,它的key為1,在舊的一組子節(jié)點(diǎn)中找到具有相同key值的可復(fù)用節(jié)點(diǎn),能夠找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中索引為0;
到了這一步發(fā)現(xiàn)遞增的順序被打破了。節(jié)點(diǎn) p - 1 在舊的一組children 的索引為0,它小于 p - 3 在舊children中的索引2.這說明節(jié)點(diǎn) p - 1 在舊children中排在 p - 3前面,但在新的children中,它排在節(jié)點(diǎn) p - 3后面。因此得出:節(jié)點(diǎn)p - 1對(duì)應(yīng)的真實(shí)DOM需要移動(dòng)
第三步:取新的一組子節(jié)點(diǎn)中的第一個(gè)節(jié)點(diǎn) p - 2,它的key為2,在舊的一組子節(jié)點(diǎn)中找到具有相同key值的可復(fù)用節(jié)點(diǎn),能夠找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中索引為1;
節(jié)點(diǎn) p - 2 在舊的一組children 的索引為0,它小于 p - 3 在舊children中的索引2.這說明節(jié)點(diǎn) p - 2 在舊children中排在 p - 3前面,但在新的children中,它排在節(jié)點(diǎn) p - 3后面。因此得出:**節(jié)點(diǎn)p - 2對(duì)應(yīng)的真實(shí)DOM需要移動(dòng)
可以將節(jié)點(diǎn) p - 3 在舊children中的索引定義為:在舊children中尋找具有相同key值節(jié)點(diǎn)的過程中,遇到的最大索引值
如果后續(xù)尋找過程中,存在比當(dāng)前遇到的最大索引值還要小的節(jié)點(diǎn),則意味著該節(jié)點(diǎn)需要移動(dòng)。
實(shí)現(xiàn)
function easyBigIndex (n1, n2, container) {
// 取出新舊子節(jié)點(diǎn)列表
const oldChildren = n1.children
const newChildren = n2.children
// 用來存儲(chǔ)尋找過程中遇到的最大索引值
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移動(dòng)
} else {
// 更新lastIndex的值(lastIndex要保持當(dāng)前已查找的索引中的最大值)
lastIndex = j
}
break
}
}
}
}如何移動(dòng)元素
移動(dòng)節(jié)點(diǎn)指的是,移動(dòng)一個(gè)虛擬節(jié)點(diǎn)所對(duì)應(yīng)的真實(shí)DOM節(jié)點(diǎn),并不是移動(dòng)虛擬節(jié)點(diǎn)本身。既然移動(dòng)的是真實(shí)DOM節(jié)點(diǎn),就需要取得它的引用,其對(duì)應(yīng)的真實(shí)DOM節(jié)點(diǎn)會(huì)存儲(chǔ)到它的vNode.el屬性中
例子
引用上面的案例:
取新的一組子節(jié)點(diǎn)中的第一個(gè)節(jié)點(diǎn) p - 3,它的key 為3,在舊的虛擬節(jié)點(diǎn)列表中找到具有相同 key 值的可復(fù)用節(jié)點(diǎn)。發(fā)現(xiàn)能夠找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中的素引為2。此時(shí)變量 lastIndex 的值為 0,索引2 不小于0,所以節(jié)點(diǎn) p - 3對(duì)應(yīng)的真實(shí)DOM 不需要移動(dòng),但需要更新變量 lastIndex 的值為 2。
第二步:取新的一組子節(jié)點(diǎn)中第二個(gè)節(jié)點(diǎn) p - 1,它的key 為1,在舊的一組子節(jié)點(diǎn)中找到具有相同 key 值的可復(fù)用節(jié)點(diǎn)。發(fā)現(xiàn)能夠找到,并且該節(jié)點(diǎn)在日的一組子節(jié)點(diǎn)中的索引為0。此時(shí)變量 lastIndex 的值為 2,索引0小于 2,所以節(jié)點(diǎn)p-1對(duì)應(yīng)的真實(shí) DOM需要移動(dòng)
到了這一步,我們發(fā)現(xiàn),節(jié)點(diǎn)p - 1對(duì)應(yīng)的真實(shí) DOM 需要移動(dòng),但應(yīng)該移動(dòng)到哪里呢?新children 的順序其實(shí)就是更新后真實(shí) DOM 節(jié)點(diǎn)應(yīng)有的順序。所以節(jié)點(diǎn) p-1在新 children 中的位置就代表了真實(shí) DOM 更新后的位置。由于節(jié)點(diǎn) p - 1在新 children 中排在節(jié)點(diǎn)p - 3后面,所以我們應(yīng)該把節(jié)點(diǎn)p - 1所對(duì)應(yīng)的真實(shí) DOM移動(dòng)到節(jié)點(diǎn)p - 3所對(duì)應(yīng)的真實(shí) DOM 后面。這樣操作之后,此時(shí)真實(shí) DOM 的順序?yàn)?p-2、p-3、p-1。
第三步:取新的一組子節(jié)點(diǎn)中第三個(gè)節(jié)點(diǎn) p-2,它的key 為2。嘗試在舊的一組子節(jié)點(diǎn)中找到具有相同 key 值的可復(fù)用節(jié)點(diǎn)。發(fā)現(xiàn)能夠找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中的素引為1。此時(shí)變量 lastIndex 的值為 2,索引1小于2,所以節(jié)點(diǎn)p-2對(duì)應(yīng)的真實(shí) DOM需要移動(dòng)
第二步操作完成后 新 / 舊 / 虛擬 節(jié)點(diǎn)之間的對(duì)應(yīng)關(guān)系

實(shí)現(xiàn)
function easyMove (n1, n2, container) {
// 取出新舊子節(jié)點(diǎn)列表
const oldChildren = n1.children
const newChildren = n2.children
// 用來存儲(chǔ)尋找過程中遇到的最大索引值
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
for (let j = 0; j < oldChildren; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移動(dòng)
// 獲取當(dāng)前vNode的前一個(gè)vNode
const prevVNode = newChildren[i - 1]
// 如果 prevVNode 不存在,說明當(dāng)前vNode是第一個(gè)節(jié)點(diǎn),它不需要移動(dòng)
if (prevVNode) {
// 由于要將newVNode對(duì)用的真實(shí)DOM移動(dòng)到prevVNode對(duì)應(yīng)的真實(shí)DOM后面,
// 所以需要獲取prevVNode對(duì)應(yīng)的真實(shí)節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn),并將其作為錨點(diǎn)
const anchor = prevVNode.el.nextSibling
// 調(diào)用insert將newVNode對(duì)應(yīng)真實(shí)DOM插入到錨點(diǎn)元素前面
// insert 是通過 el.insertBefore 插入元素的
insert(newVNode.el, container, anchor)
}
} else {
// 更新lastIndex的值(lastIndex要保持當(dāng)前已查找的索引中的最大值)
lastIndex = j
}
break
}
}
}
}添加新元素
例子
在新的一組子節(jié)點(diǎn)中,多出來一個(gè) p - 4,它的key值為4,該節(jié)點(diǎn)在舊的一組字節(jié)點(diǎn)中不存在,因此應(yīng)該將其視為新增節(jié)點(diǎn)。對(duì)于新增節(jié)點(diǎn),更新時(shí)應(yīng)該正確地將其掛載:
- 找到新增節(jié)點(diǎn)
- 將新增節(jié)點(diǎn)掛載到正確位置
第一步:取新的一組子節(jié)點(diǎn)中第一個(gè)節(jié)點(diǎn)p - 3,它的key值為3,在舊的一組子節(jié)及中找到可復(fù)用的節(jié)點(diǎn)。發(fā)現(xiàn)能找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中的索引值為2。此時(shí),變量lastIndex的值為0,所以節(jié)點(diǎn) p - 3 對(duì)應(yīng)的真實(shí) DOM 不需要移動(dòng),但是需要將變量 lastIndex 的值更新為 2。
第二步:取新的一組子節(jié)點(diǎn)中第一個(gè)節(jié)點(diǎn)p - 1,它的key值為1,在舊的一組子節(jié)及中找到可復(fù)用的節(jié)點(diǎn)。發(fā)現(xiàn)能找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中的索引值為1。此時(shí)變量lastIndex的值為2,所以節(jié)點(diǎn) p - 1對(duì)應(yīng)的真實(shí)DOM需要移動(dòng),并且應(yīng)該移動(dòng)到節(jié)點(diǎn) p - 3對(duì)應(yīng)的真實(shí)DOM后面。
第三步:取新的一組子節(jié)點(diǎn)中第一個(gè)節(jié)點(diǎn)p - 4,它的key值為4,在舊的一組子節(jié)及中找到可復(fù)用的節(jié)點(diǎn)。沒有key值為4的節(jié)點(diǎn),因此渲染器會(huì)把節(jié)點(diǎn) p - 4 看作新增節(jié)點(diǎn)并掛載它。應(yīng)該掛載到什么地方呢?觀察p - 4在新的一組子節(jié)點(diǎn)中的位置。由于 p - 4出現(xiàn)在節(jié)點(diǎn) p - 1后面,所以應(yīng)該把 p - 4 掛載到節(jié)點(diǎn) p - 1 對(duì)應(yīng)的真實(shí)DOM后面。
第四步:取新的一組子節(jié)點(diǎn)中第一個(gè)節(jié)點(diǎn)p - 2,它的key值為2,在舊的一組子節(jié)及中找到可復(fù)用的節(jié)點(diǎn)。發(fā)現(xiàn)能找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中的索引值為1。此時(shí),變量lastIndex的值為2,索引值1小于lastIndex的值2,所以節(jié)點(diǎn) p - 2對(duì)應(yīng)的真實(shí)DOM需要移動(dòng),并且應(yīng)該移動(dòng)到節(jié)點(diǎn) p - 4對(duì)應(yīng)的真實(shí)DOM后面。
第二步操作完成后的節(jié)點(diǎn)對(duì)應(yīng)關(guān)系

第三步操作完成后的節(jié)點(diǎn)對(duì)應(yīng)關(guān)系

實(shí)現(xiàn)
function easyMount (n1, n2, container) {
// 取出新舊子節(jié)點(diǎn)列表
const oldChildren = n1.children
const newChildren = n2.children
// 用來存儲(chǔ)尋找過程中遇到的最大索引值
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
// 定義變量 find,代表是否在舊的一組子節(jié)點(diǎn)中找到可復(fù)用的節(jié)點(diǎn),初始值為false - 沒找到
let find = false
for (let j = 0; j < oldChildren; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
// 一旦找到可復(fù)用的節(jié)點(diǎn),將變量find設(shè)置為true
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移動(dòng)
// 獲取當(dāng)前vNode的前一個(gè)vNode
const prevVNode = newChildren[i - 1]
// 如果 prevVNode 不存在,說明當(dāng)前vNode是第一個(gè)節(jié)點(diǎn),它不需要移動(dòng)
if (prevVNode) {
// 由于要將newVNode對(duì)用的真實(shí)DOM移動(dòng)到prevVNode對(duì)應(yīng)的真實(shí)DOM后面,
// 所以需要獲取prevVNode對(duì)應(yīng)的真實(shí)節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn),并將其作為錨點(diǎn)
const anchor = prevVNode.el.nextSibling
// 調(diào)用insert將newVNode對(duì)應(yīng)真實(shí)DOM插入到錨點(diǎn)元素前面
// insert 是通過 el.insertBefore 插入元素的
insert(newVNode.el, container, anchor)
}
} else {
// 更新lastIndex的值(lastIndex要保持當(dāng)前已查找的索引中的最大值)
lastIndex = j
}
break
}
}
// 這里find如果還是false,說明當(dāng)前newVNode沒有在舊的一組子節(jié)點(diǎn)中找到可復(fù)用的節(jié)點(diǎn)
// 也就是說當(dāng)前 newVNode 是新增節(jié)點(diǎn),需要掛載
if (!find) {
// 為了將節(jié)點(diǎn)掛載到正確位置,需要先獲取錨點(diǎn)元素
// 首先獲取當(dāng)前newVNode的前一個(gè)vNode節(jié)點(diǎn)
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
// 如果有前一個(gè)vNode節(jié)點(diǎn),則使用它的下一個(gè)兄弟節(jié)點(diǎn)作為錨點(diǎn)元素
anchor = prevVNode.el.nextSibling
} else {
// 如果沒有前一個(gè)vNode節(jié)點(diǎn),說明即將掛載的新節(jié)點(diǎn)是第一個(gè)子節(jié)點(diǎn)
// 這是使用容器元素的firstChild作為錨點(diǎn)
anchor = container.firstChild
}
// 掛載 newVNode
patch(null, newVNode, container, anchor)
}
}
}移除不存在的元素
例子
在新的一組節(jié)點(diǎn)中,節(jié)點(diǎn) p - 2 不存在了,說明該節(jié)點(diǎn)被刪除,渲染器應(yīng)該能找到那些需要?jiǎng)h除的節(jié)點(diǎn)并正確地將其刪除。
找到需要?jiǎng)h除的節(jié)點(diǎn) - 步驟:
- 第一步:取新的一組子節(jié)點(diǎn)中的第一個(gè)節(jié)點(diǎn)p - 3,它的key 值為3,在舊的一組子節(jié)點(diǎn)中尋找可復(fù)用的節(jié)點(diǎn)。發(fā)現(xiàn)能夠找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中的索引值為2。此時(shí)變量 lastIndex 的值為0,索引2不小于lastIndex 的值0,所以節(jié)點(diǎn)p - 3對(duì)應(yīng)的真實(shí) DOM 不需要移動(dòng),但需要更新變量 lastIndex 的值為 2。
- 第二步:取新的一組子節(jié)點(diǎn)中的第二個(gè)節(jié)點(diǎn) p - 1,它的key 值為1。嘗試在舊的一組子節(jié)點(diǎn)中尋找可復(fù)用的節(jié)點(diǎn)。發(fā)現(xiàn)能夠找到,并且該節(jié)點(diǎn)在舊的一組子節(jié)點(diǎn)中的索引值為0。此時(shí)變量 lastIndex 的值為2,索引0小于 lastIndex 的值 2, 所以節(jié)點(diǎn)p-1對(duì)應(yīng)的真實(shí) DOM需要移動(dòng),并且應(yīng)該移動(dòng)到節(jié)點(diǎn)p-3對(duì)應(yīng)的真實(shí) DOM 后面經(jīng)過這一步的移動(dòng)操作后,發(fā)現(xiàn) p - 3和p - 1都有了對(duì)應(yīng)的真實(shí)DOM節(jié)點(diǎn)。
- 至此更新結(jié)束,但 p - 2 對(duì)應(yīng)的真實(shí)DOM仍然存在,所以需要增加額外的邏輯來刪除遺留節(jié)點(diǎn)。當(dāng)基本的更新結(jié)束時(shí),需要遍歷舊的一組子節(jié)點(diǎn),然后去新的一組子節(jié)點(diǎn)中尋找具有相同key值的節(jié)點(diǎn)。如果找不到,說明應(yīng)該刪除該節(jié)點(diǎn)。
p - 2與任何newVNode沒有對(duì)應(yīng)關(guān)系

實(shí)現(xiàn)
function easyUnmount (n1, n2, container) {
// 取出新舊子節(jié)點(diǎn)列表
const oldChildren = n1.children
const newChildren = n2.children
// 用來存儲(chǔ)尋找過程中遇到的最大索引值
let lastIndex = 0
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i]
// 定義變量 find,代表是否在舊的一組子節(jié)點(diǎn)中找到可復(fù)用的節(jié)點(diǎn),初始值為false - 沒找到
let find = false
for (let j = 0; j < oldChildren; j++) {
const oldVNode = oldChildren[j]
if (newVNode.key === oldVNode.key) {
// 一旦找到可復(fù)用的節(jié)點(diǎn),將變量find設(shè)置為true
find = true
patch(oldVNode, newVNode, container)
if (j < lastIndex) {
// 需要移動(dòng)
// 獲取當(dāng)前vNode的前一個(gè)vNode
const prevVNode = newChildren[i - 1]
// 如果 prevVNode 不存在,說明當(dāng)前vNode是第一個(gè)節(jié)點(diǎn),它不需要移動(dòng)
if (prevVNode) {
// 由于要將newVNode對(duì)用的真實(shí)DOM移動(dòng)到prevVNode對(duì)應(yīng)的真實(shí)DOM后面,
// 所以需要獲取prevVNode對(duì)應(yīng)的真實(shí)節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn),并將其作為錨點(diǎn)
const anchor = prevVNode.el.nextSibling
// 調(diào)用insert將newVNode對(duì)應(yīng)真實(shí)DOM插入到錨點(diǎn)元素前面
// insert 是通過 el.insertBefore 插入元素的
insert(newVNode.el, container, anchor)
}
} else {
// 更新lastIndex的值(lastIndex要保持當(dāng)前已查找的索引中的最大值)
lastIndex = j
}
break
}
}
// 這里find如果還是false,說明當(dāng)前newVNode沒有在舊的一組子節(jié)點(diǎn)中找到可復(fù)用的節(jié)點(diǎn)
// 也就是說當(dāng)前 newVNode 是新增節(jié)點(diǎn),需要掛載
if (!find) {
// 為了將節(jié)點(diǎn)掛載到正確位置,需要先獲取錨點(diǎn)元素
// 首先獲取當(dāng)前newVNode的前一個(gè)vNode節(jié)點(diǎn)
const prevVNode = newChildren[i - 1]
let anchor = null
if (prevVNode) {
// 如果有前一個(gè)vNode節(jié)點(diǎn),則使用它的下一個(gè)兄弟節(jié)點(diǎn)作為錨點(diǎn)元素
anchor = prevVNode.el.nextSibling
} else {
// 如果沒有前一個(gè)vNode節(jié)點(diǎn),說明即將掛載的新節(jié)點(diǎn)是第一個(gè)子節(jié)點(diǎn)
// 這是使用容器元素的firstChild作為錨點(diǎn)
anchor = container.firstChild
}
// 掛載 newVNode
patch(null, newVNode, anchor)
}
}
// 更新操作完成后,遍歷舊的一組子節(jié)點(diǎn)
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i]
// 拿舊子節(jié)點(diǎn)oldVNode去新的一組子節(jié)點(diǎn)中尋找具有相同key值的節(jié)點(diǎn)
const has = newChildren.find(
vNode => vNode.key === oldVNode.key
)
if (has) {
// 如果沒找到具有相同key值的節(jié)點(diǎn),則說明需要?jiǎng)h除該節(jié)點(diǎn),調(diào)用unmount函數(shù)將其卸載
unmount(oldVNode)
}
}
}總結(jié)
遍歷新舊子結(jié)點(diǎn)中較少的一組,逐個(gè)調(diào)用patch進(jìn)行打補(bǔ)丁,然后比較新舊兩組子節(jié)點(diǎn)的數(shù)量,如果新的一組子節(jié)點(diǎn)數(shù)量更多,說明有新節(jié)點(diǎn)需要掛載;否則說明在舊的一組子節(jié)點(diǎn)中,有節(jié)點(diǎn)需要卸載。
引入key屬性,就像虛擬節(jié)點(diǎn)的身份證號(hào),通過key找到可復(fù)用的節(jié)點(diǎn),然后盡可能通過DOM移動(dòng)操作來完成更新,避免過多地對(duì)DOM元素進(jìn)行銷毀和重建。
簡單Diff算法地核心邏輯是,拿新的一組子節(jié)點(diǎn)中地節(jié)點(diǎn)去舊的一組子節(jié)點(diǎn)中尋找可復(fù)用地節(jié)點(diǎn),如果找到了,則記錄該節(jié)點(diǎn)地位置索引。在整個(gè)更新過程中,如果一個(gè)節(jié)點(diǎn)地索引值小于最大索引,則說明該節(jié)點(diǎn)對(duì)應(yīng)地真實(shí)DOM元素需要移動(dòng)。
以上就是一文詳解Vue3中簡單diff算法的實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Vue簡單diff算法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
VUE在for循環(huán)里面根據(jù)內(nèi)容值動(dòng)態(tài)的加入class值的方法
這篇文章主要介紹了VUE在for循環(huán)里面根據(jù)內(nèi)容值動(dòng)態(tài)的加入class值的方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-08-08
vue把頁面轉(zhuǎn)換成圖片導(dǎo)出方式(html2canvas導(dǎo)出不全問題)
這篇文章主要介紹了vue把頁面轉(zhuǎn)換成圖片導(dǎo)出方式(html2canvas導(dǎo)出不全問題),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10
Vue3項(xiàng)目頁面實(shí)現(xiàn)echarts圖表漸變色的動(dòng)態(tài)配置的實(shí)現(xiàn)步驟
在開發(fā)可配置業(yè)務(wù)平臺(tái)時(shí),需要實(shí)現(xiàn)讓用戶對(duì)項(xiàng)目內(nèi)echarts圖表的動(dòng)態(tài)配置,讓用戶脫離代碼也能實(shí)現(xiàn)簡單的圖表樣式配置,顏色作為圖表樣式的重要組成部分,其配置方式是項(xiàng)目要解決的重點(diǎn)問題,所以本文介紹了Vue3項(xiàng)目頁面實(shí)現(xiàn)echarts圖表漸變色的動(dòng)態(tài)配置2024-10-10
對(duì)vue下點(diǎn)擊事件傳參和不傳參的區(qū)別詳解
今天小編就為大家分享一篇對(duì)vue下點(diǎn)擊事件傳參和不傳參的區(qū)別詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09
vue項(xiàng)目index.html中使用環(huán)境變量的代碼示例
在Vue3中使用環(huán)境變量的方式與Vue2基本相同,下面這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目index.html中使用環(huán)境變量的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-02-02
基于Vite2.x的Vue 3.x項(xiàng)目的搭建實(shí)現(xiàn)
這篇文章主要介紹了基于Vite2.x的Vue 3.x項(xiàng)目的搭建實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04

