Vue2?的?diff?算法規(guī)則原理詳解
前言
所謂 diff 算法,就是通過比對新舊兩個虛擬節(jié)點不一樣的地方,針對那些不一樣的地方進行新增或更新或刪除操作。接下來我們詳細介紹節(jié)點更新的過程。
首先進行靜態(tài)節(jié)點處理,判斷新舊兩個虛擬節(jié)點是否是靜態(tài)節(jié)點,如果是,就不需要進行更新操作,可以直接跳過更新比對的過程 。
再更新處理新老節(jié)點的屬性,獲取新老節(jié)點的子節(jié)點,然后進行一定規(guī)則的判斷。
這里值得多說一下的是,Vue2 在更新元素屬性的時候,是暴力全量 diff 更新的,Vue3 則做了很多優(yōu)化。
算法規(guī)則
具體規(guī)則如下:
- 如果新節(jié)點有子節(jié)點而老節(jié)點沒有子節(jié)點,則判斷老節(jié)點是否有文本內(nèi)容,如果有就清空老節(jié)點的文本內(nèi)容,然后為其新增子節(jié)點。
- 如果新節(jié)點沒有子節(jié)點而老節(jié)點有子節(jié)點,則先刪除老節(jié)點的子節(jié)點,然后設置文本內(nèi)容。
- 如果新節(jié)點沒有子節(jié)點,老節(jié)點也沒有子節(jié)點,則進行文本的比對,然后設置文本內(nèi)容。
- 如果新節(jié)點有子節(jié)點,老節(jié)點也有子節(jié)點,則進行新老子節(jié)點的比對,然后進行新增、移動、刪除的操作,這也就是傳說中的 diff 算法發(fā)生的地方。
patchVnode 源碼解析:
// diff 的過程
// 分析當前兩個節(jié)點的類型
// 如果是元素,更新雙方屬性、特性等,同時比較雙方子元素,這個遞歸過程,叫深度優(yōu)先
// 如果雙方是文本,更新文本
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
// 靜態(tài)節(jié)點處理
// 判斷新舊兩個虛擬節(jié)點是否是靜態(tài)節(jié)點,如果是,就不需要進行更新操作,可以直接跳過更新比對的過程
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
// 獲取雙方孩子
const oldCh = oldVnode.children
const ch = vnode.children
// 比較雙方屬性
// Vue2在更新元素屬性的時候,是暴力全量 diff 更新的。Vue3 則做了很多優(yōu)化。
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 根據(jù)雙方類型的幾種情況分別處理
if (isUndef(vnode.text)) {// 新節(jié)點沒有文本
if (isDef(oldCh) && isDef(ch)) {
// 雙方都有子元素,就進行重排,傳說中的 diff 就發(fā)生在這里
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 新節(jié)點有孩子, 老的沒有,新增創(chuàng)建
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 判斷老節(jié)點是否有文本內(nèi)容,如果有則先清空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 批量添加子節(jié)點
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 新節(jié)點沒有孩子,老的有的,則刪除老節(jié)點的孩子節(jié)點
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 新節(jié)點沒有文本節(jié)點,老的有文本節(jié)點,則清空老的文本節(jié)點
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新老節(jié)點都是文本節(jié)點,則判斷新老文本內(nèi)容是否相同進行文本更新
nodeOps.setTextContent(elm, vnode.text)
}
// 鉤子處理
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
接下來,我們看看兩組子元素都是多節(jié)點比對的情況,也就是傳說 diff 發(fā)生的地方。

在新老兩組VNode節(jié)點的左右頭尾兩側都有一個變量標記,在遍歷過程中這幾個變量都會向中間靠攏,當oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx時結束循環(huán)。
diff 優(yōu)化策略
先進行以下4種情況的優(yōu)化策略:
- 老數(shù)組的開始與新數(shù)組的開始:oldStartVnode, newStartVnode
- 老數(shù)組的結尾與新數(shù)組的結尾:oldEndVnode, newEndVnode
- 老數(shù)組的開始與新數(shù)組的結尾:oldStartVnode, newEndVnode
- 老數(shù)組的結尾與新數(shù)組的開始:oldEndVnode, newStartVnode
如果以上4種情況都沒找到,則從新數(shù)組的第一個節(jié)點去老數(shù)組中去查找,找到了就進行遞歸更新,沒找到則創(chuàng)建新節(jié)點。
老數(shù)組的開始與新數(shù)組的開始
新數(shù)組的結尾節(jié)點有剩余則添加

從左往右比對完,老數(shù)組的游標先相交了,發(fā)現(xiàn)新數(shù)組結尾還有節(jié)點沒有比對,則在新數(shù)組結尾創(chuàng)建剩下沒有比對的節(jié)點。
老數(shù)組的結尾節(jié)點有剩余則刪除

從左往右比對完,新數(shù)組的游標先相交了,發(fā)現(xiàn)老數(shù)組結尾還有節(jié)點沒有比對,則刪除老數(shù)組剩下沒有比對的節(jié)點。
老數(shù)組的結尾與新數(shù)組的結尾
新數(shù)組的開頭節(jié)點有剩余則添加

從右往左比對完,老數(shù)組的游標先相交了,發(fā)現(xiàn)新數(shù)組開頭還有節(jié)點沒有比對,則在新數(shù)組開頭創(chuàng)建沒有比對的節(jié)點。
老數(shù)組的開頭節(jié)點有剩余則刪除

從右往左比對完,新數(shù)組的游標先相交了,發(fā)現(xiàn)老數(shù)組的開頭還有節(jié)點沒有比對,則刪除老數(shù)組開頭沒有比對的節(jié)點。
老數(shù)組的開始與新數(shù)組的結尾

如果老數(shù)組的開頭節(jié)點與新數(shù)組的結尾節(jié)點比對成功了,除了會繼續(xù)遞歸比對它們,還將真實節(jié)點 A 移動到結尾。
老數(shù)組的結尾與新數(shù)組的開始

如果老數(shù)組的結尾節(jié)點與新數(shù)組的開始節(jié)點比對成功了,除了會繼續(xù)遞歸比對它們,還將真實節(jié)點D移動到開頭。
以上四種情況都沒對比成功
如果以上4種情況都沒找到,則拿新數(shù)組的第一個節(jié)點去老數(shù)組中去查找。

如果拿新數(shù)組的第一個節(jié)點去老數(shù)組中查找成功了,則會繼續(xù)遞歸比對它們,同時將比對到的節(jié)點移動到對應的節(jié)點前面,并且將老數(shù)組原來的位置內(nèi)容設置為 undefind。

如果拿新數(shù)組的第一個節(jié)點去老數(shù)組中查找,沒找到,則創(chuàng)建一個新的節(jié)點插入到未處理的節(jié)點前面。
推薦在渲染列表時為節(jié)點設置 key
如果我們在模版渲染列表時,為節(jié)點設置了屬性 key,那么在上面建立的 key 與 index 索引的對應關系時,就生成了一個 key 對應著一個節(jié)點下標這樣一個對象。 也就是說,如果在節(jié)點上設置了屬性 key,那么在老的虛擬DOM中找相同節(jié)點時,可以直接通過 key 拿到下標,從而獲取節(jié)點,否則我們就需要每一次都要進行遍歷查找。 所以非常推薦在渲染列表時為節(jié)點設置 key,最好是后端返回的唯一 ID。
循環(huán)比對結束的后續(xù)處理工作
如果老數(shù)組的游標先相交了,則判斷新數(shù)組中是否還有剩下的節(jié)點,沒有進行比對的,創(chuàng)建它們。
如果新數(shù)組的游標先相交了,則判斷老數(shù)組中是否還有剩下的節(jié)點,沒有進行比對的,把它們都刪除掉。
源碼解析
// 傳說中的 diff 發(fā)生的地方
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 4個游標和對應節(jié)點
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
// 后續(xù)查找需要的變量
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
// 循環(huán)條件是游標不能交叉,交叉就結束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 前兩個是校正,在之前的比對中可能會刪除其中的舊節(jié)點,之后就會往前或者往后移動一位
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 先查找兩個開頭節(jié)點
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 兩個結尾節(jié)點
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 老的開始節(jié)點,新的結尾節(jié)點
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 進行節(jié)點移動
// node.insertBefore(newnode,existingnode) 1.newnode 必需。需要插入的節(jié)點對象 2.existingnode 可選。在其之前插入新節(jié)點的子節(jié)點。如果未規(guī)定,則 insertBefore 方法會在結尾插入 newnode。
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 老的結尾節(jié)點,新的開始節(jié)點
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 進行節(jié)點移動
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 首尾沒找到
// 第一次創(chuàng)建一個老的節(jié)點的索引 Map,方便后續(xù)不需要遍歷查找,這是一個空間換時間的方法
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 拿新虛擬DOM開頭的第一個節(jié)點,去老的虛擬DOM中進行查找
// 如果我們在模版渲染列表時,為節(jié)點設置了屬性 key,那么在上面建立的 key 與 index 索引的對應關系時,就生成了一個 key 對應著一個節(jié)點下標這樣一個對象。
// 也就是說,如果在節(jié)點上設置了屬性 key,那么在老的虛擬DOM中找相同節(jié)點時,可以直接通過 key 拿到下標,從而獲取節(jié)點,否則我們就需要每一次都要進行遍歷查找。
// 所以非常推薦在渲染列表時為節(jié)點設置 key,最好是后端返回的唯一 ID。
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
// 沒找到就進行創(chuàng)建,并且插入到未處理的節(jié)點(oldStartVnode.elm)的前面
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
// 找到之后,也要進行判斷是否相同節(jié)點
if (sameVnode(vnodeToMove, newStartVnode)) {
// 遞歸更新
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
// 創(chuàng)建新的節(jié)點進行替換
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// 循環(huán)結束
// 后續(xù)處理工作
if (oldStartIdx > oldEndIdx) {
// 老的先結束,判斷新的虛擬DOM中是否還有剩下的節(jié)點,批量創(chuàng)建
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 新的先結束,判斷老的虛擬DOM中是否還剩下,批量刪除
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}總結
總的來說 Vue2 的 diff 算法就是以新的虛擬DOM為準進行與老虛擬DOM的比對,繼而進行各種情況的處理。大概可以分為 4 種情況:更新節(jié)點、新增節(jié)點、刪除節(jié)點、移動節(jié)點位置。比對新老兩個虛擬DOM,就是通過循環(huán),每循環(huán)到一個新節(jié)點,就去老節(jié)點列表里面找到和當前新節(jié)點相同的舊節(jié)點。如果在舊節(jié)點列表中找不到,說明當前節(jié)點是需要新增的節(jié)點,我們就需要進行創(chuàng)建節(jié)點并插入視圖的操作;如果找到了,就做更新操作;如果找到的舊節(jié)點與新節(jié)點位置不同,則需要移動節(jié)點等。
其中為了快速查找到節(jié)點,Vue2 的 diff 算法設置了 4 種優(yōu)化策略,分別是:
- 老數(shù)組的開始與新數(shù)組的開始
- 老數(shù)組的結尾與新數(shù)組的結尾
- 老數(shù)組的開始與新數(shù)組的結尾
- 老數(shù)組的結尾與新數(shù)組的開始
通過這 4 種快捷的查找方式,我們就不需要循環(huán)來查找了,只有當以上 4 種方式都查找不到的時候,再進行循環(huán)查找。
最后循環(huán)結束后需要對未處理的節(jié)點進行處理。
如果是老節(jié)點列表先循環(huán)完畢,這個時候如果新節(jié)點列表還有剩余的節(jié)點,則說明這些節(jié)點都是需要新增的節(jié)點,直接把這些節(jié)點創(chuàng)建并插入到 DOM 中就行了。
如果是新節(jié)點列表先循環(huán)完畢,這個時候如果老節(jié)點列表還有剩余節(jié)點,則說明這些節(jié)點都是要被廢棄的節(jié)點,是應該被刪除的節(jié)點,直接批量刪除就可以了。
到此這篇關于Vue2 的 diff 算法規(guī)則原理詳解的文章就介紹到這了,更多相關Vue2 diff 算法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Vue監(jiān)聽一個數(shù)組id是否與另一個數(shù)組id相同的方法
今天小編就為大家分享一篇Vue監(jiān)聽一個數(shù)組id是否與另一個數(shù)組id相同的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09

