Vue2.0
的Virtual DOM
是基于Snabbdom
改造的赛蔫,針對Children
的Diff
過程裤唠,做了一下詳細的過程分析以及演示挤牛。
-
前言
?? 瀏覽器在渲染
DOM
元素的時候,是非持终海‘昂貴的’墓赴,在進行DOM
更新的時候,針對復(fù)雜DOM
的局部更新航瞭,為了節(jié)省瀏覽器的開支诫硕,避免不必要的更新,這個時候就需要通過Diff
比較來決定更新哪些DOM
元素刊侯,當(dāng)然章办,并不是所有的情況都能使Virtual DOM
比原生DOM
操作要快,這里我就不過多闡述了滨彻,好了藕届,進入主題
1.0 Snabbdom Diff 方式
?? Snabbdom
在進行 Diff
比較的時候只針對同層級進行比較,而不會進行跨層級比較亭饵,也就是說休偶,我不會管你當(dāng)前的 Children
是否一致,這里我只關(guān)心你和他辜羊。Snabbdom
更新DOM
的方式是 先移 后添加或刪除踏兜, Diff
比較是雙向由外向里進行比較。
為了方便演示
Snabbdom
的Diff
的過程八秃,這里我以一個DOM
更新的案例為大家講解
??假設(shè)當(dāng)前
DOM
parentElm
有9個DOM
元素碱妆,
??這里我用數(shù)組模擬oldCh = [null, "A", "B", "C", "D", "E", "F", "G", null]
表示,
??其中數(shù)組的每一個元素代表一個子節(jié)點DOM
喜德,其中兩個null
山橄,表示已經(jīng)移除了,這里加入null
的目的是為了后續(xù)分析源碼流程所使用。
??更新后
DOM
parentElm
有11個DOM
元素航棱,
??這里我用數(shù)組模擬newCh = [null, "A", "F", "E", "M", "O", "I", "E", "B", "G", null]
表示睡雇,
??其中數(shù)組的每一個元素代表一個子節(jié)點DOM
,其中兩個null
饮醇,表示已經(jīng)移除了它抱,這里加入null
的目的是為了后續(xù)分析源碼流程所使用。
2.0 Snabbdom Diff 過程
??首先朴艰,我用一張圖來解釋Snabbdom
在找出差異之前是如何進行Diff
比較的
??Snabbdom
在找出差異之前观蓄,每一輪的比較是需要經(jīng)過8項條件比較的,具體比較【條件】如下:
??【1】.判斷
oldCh
第一個節(jié)點元素是不是空的祠墅,如果是空的表示DOM
已經(jīng)移除侮穿,接著進行下一輪比較
??【2】.判斷oldCh
最后一個節(jié)點元素是不是空的,如果是空的表示DOM
已經(jīng)移除毁嗦,接著進行下一輪比較
??【3】.判斷newCh
第一個節(jié)點元素是不是空的亲茅,如果是空的表示DOM
已經(jīng)移除,接著進行下一輪比較
??【4】.判斷newCh
最后一個節(jié)點元素是不是空的狗准,如果是空的表示DOM
已經(jīng)移除克锣,接著進行下一輪比較。
??【5】.判斷oldCh
與newCh
第一個節(jié)點元素是不是相同腔长,如果相同我們接著進行下一輪比較
??【6】.判斷oldCh
與newCh
最后一個節(jié)點元素是不是相同袭祟,如果相同我們接著進行下一輪比較
??【7】.判斷oldCh
第一個節(jié)點元素與newCh
的最后一個節(jié)點元素是否相同,如果相同捞附,就將oldCh
第一個節(jié)點元素進行移動巾乳,具體如何移動,后面我會詳細闡述說明故俐。
??【8】.判斷oldCh
最后一個節(jié)點元素與newCh
的第一個節(jié)點元素是否相同想鹰,如果相同,就將oldCh
最后一個節(jié)點元素進行移動药版,具體如何移動辑舷,后面我會詳細闡述說明。
??由于 Diff
比較過程是雙向由外向里進行比較槽片,所以為了比較過程這里我們設(shè)置幾個記錄值
??為了記錄比較過程這里我們設(shè)置幾個記錄值
??oldSIdx
:初始值為0何缓,表示當(dāng)前oldCh
比較隊列的 初始的節(jié)點位置
??newSIdx
:初始值為0,表示當(dāng)前newCh
比較隊列的 初始的節(jié)點位置
??oldEIdx
: 初始值為oldCh.length-1
还栓,表示當(dāng)前oldCh
比較隊列的末尾的節(jié)點位置
??newEIdx
: 初始值為newCh.length-1
碌廓,表示當(dāng)前newCh
比較隊列的末尾的節(jié)點位置
??oldSnode
:初始值為oldCh[0]
,表示當(dāng)前oldCh
比較隊列的初始節(jié)點
??oldEnode
:初始值為oldCh[oldEIdx]
剩盒,表示當(dāng)前oldCh
比較隊列的末尾節(jié)點
??newSnode
:初始值為newCh[0]
谷婆,表示當(dāng)前newCh
比較隊列的初始節(jié)點
??newEnode
:初始值為newCh[newEIdx]
,表示當(dāng)前newCh
比較隊列的末尾節(jié)點
??每一輪的
Diff
執(zhí)行條件:oldSIdx <= oldEIdx && newSIdx <= newEIdx
,也就是說纪挎,當(dāng)兩邊的比較有一方的元素已經(jīng)全部比較完畢期贫,那么Diff
中止
??初始狀態(tài) :
?? 第1輪Diff
比較:滿足條件【1】 oldCh
第一個節(jié)點元素是空,執(zhí)行以下步驟异袄, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
oldSIdx
位置加1通砍,oldSnode
指向下一個節(jié)點>oldSnode=[++oldSIdx]
??第1輪Diff
比較結(jié)束后,oldCh
初始記錄點oldSIdx
索引+1烤蜕,由0
變?yōu)?code>1封孙,oldSnode
指向下一個節(jié)點,由null
變?yōu)?code>A讽营。
?? 第2輪Diff
比較:滿足條件【2】 oldCh
最一個節(jié)點元素是空虎忌,,執(zhí)行以下步驟橱鹏, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
oldEIdx
位置減1呐籽,oldEnode
指向上一個節(jié)點>oldEnode=[--oldEIdx]
??第2輪Diff
比較結(jié)束后,oldCh
末尾記錄點oldEIdx
索引-1蚀瘸,由8
變?yōu)?code>7,oldEnode
指向下一個節(jié)點庶橱,由null
變?yōu)?code>G贮勃。
?? 第3Diff
輪比較:滿足條件【3】 newCh
第一個節(jié)點元素是空,執(zhí)行以下步驟苏章, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
newSIdx
位置加1寂嘉,newSnode
指向下一個節(jié)點>newSnode=[++newSIdx]
??第3輪Diff
比較結(jié)束后,newCh
初始記錄點newSIdx
索引+1枫绅,由0
變?yōu)?code>1泉孩,newSnode
指向下一個節(jié)點,由null
變?yōu)?code>A并淋。
?? 第4輪Diff
比較:滿足條件【4】 newCh
最一個節(jié)點元素是空寓搬,執(zhí)行以下步驟, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
newEIdx
位置減1县耽,newEnode
指向上一個節(jié)點>newEnode=[--newEIdx]
??第4輪Diff
比較結(jié)束后句喷,newCh
末尾記錄點newEIdx
索引-1,由10
變?yōu)?code>9兔毙,newEnode
指向下一個節(jié)點唾琼,由null
變?yōu)?code>G。
?? 第5輪Diff
比較:滿足條件【5】 oldCh
與 newCh
第一個節(jié)點元素相同澎剥,執(zhí)行以下步驟锡溯, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??oldSIdx
位置加1,oldSnode
指向下一個節(jié)點>oldSnode=[++oldSIdx]
??newSIdx
位置加1,newSnode
指向下一個節(jié)點>newSnode=[++newSIdx]
??第5輪Diff
比較結(jié)束后:
??oldCh
初始記錄點oldSIdx
索引+1祭饭,由1
變?yōu)?code>2芜茵,oldSnode
指向下一個節(jié)點,由A
變?yōu)?code>B甜癞。
??newCh
初始記錄點newSIdx
索引+1夕晓,由1
變?yōu)?code>2,newSnode
指向下一個節(jié)點悠咱,由A
變?yōu)?code>F蒸辆。
?? 第6輪Diff
比較:滿足條件【6】 oldCh
與 newCh
最后一個節(jié)點元素相同,執(zhí)行以下步驟析既, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??oldEIdx
位置減1躬贡,oldEnode
指向上一個節(jié)點>oldEnode=[--oldEIdx]
??newEIdx
位置減1,newEnode
指向上一個節(jié)點>newEnode=[--newEIdx]
??第6輪Diff
比較結(jié)束后:
??oldCh
末尾記錄點oldEIdx
索引-1眼坏,由7
變?yōu)?code>6拂玻,oldEnode
指向上一個節(jié)點,由G
變?yōu)?code>F宰译。
??newCh
末尾記錄點newEIdx
索引-1檐蚜,由9
變?yōu)?code>8,newEnode
指向上一個節(jié)點沿侈,由G
變?yōu)?code>B闯第。
?? 看到這里,不知道各位有沒有發(fā)現(xiàn)缀拭,每一輪的比較咳短,在滿足
Diff
算法的前 6 種判斷條件下,父節(jié)點parentElm
是沒有發(fā)生任何變化的蛛淋,那么咙好,接下來就是見證奇跡的時刻!
?? 第7輪Diff
比較:滿足條件【7】 oldCh
第一個節(jié)點元素與 newCh
最后一個節(jié)點元素相同褐荷,執(zhí)行以下步驟勾效, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??我們首先將當(dāng)前的oldSnode
(B)元素移動插入到當(dāng)前oldEnode
(F)元素的后面
??oldSIdx
位置加1,oldSnode
指向下一個節(jié)點>oldSnode=[++oldSIdx]
??newEIdx
位置減1诚卸,newEnode
指向上一個節(jié)點>newEnode=[--newEIdx]
??第7輪Diff
比較結(jié)束后:
??parentElm
更新DOM
葵第,將當(dāng)前的oldSnode
(B)元素移動插入到當(dāng)前oldEnode
(F)元素的后面
??oldCh
初始記錄點oldSIdx
索引+1,由2
變?yōu)?code>3合溺,oldSnode
指向下一個節(jié)點卒密,由B
變?yōu)?code>C。
??newCh
末尾記錄點newEIdx
索引-1棠赛,由8
變?yōu)?code>7哮奇,newEnode
指向上一個節(jié)點膛腐,由B
變?yōu)?code>E。
?? 第8輪Diff
比較:滿足條件【8】 oldCh
最后一個節(jié)點元素與 newCh
第一個節(jié)點元素相同鼎俘,執(zhí)行以下步驟哲身, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??我們首先將當(dāng)前的oldEnode
(F)元素插入到當(dāng)前oldSnode
(C)元素的前面
??oldEIdx
位置減1,oldEnode
指向上一個節(jié)點>oldEnode=[--oldEIdx]
??newSIdx
位置加1贸伐,newSnode
指向下一個節(jié)點>newSnode=[++newSIdx]
??第8輪Diff
比較結(jié)束后:
??parentElm
更新DOM
勘天,將當(dāng)前的oldEnode
(F)元素插入到當(dāng)前oldSnode
(C)元素的前面
??oldCh
末尾記錄點oldEIdx
索引-1,由6
變?yōu)?code>5捉邢,oldEnode
指向上一個節(jié)點脯丝,由F
變?yōu)?code>E。
??newCh
初始記錄點newSIdx
索引+1伏伐,由2
變?yōu)?code>3宠进,newSnode
指向下一個節(jié)點,由F
變?yōu)?code>E藐翎。
?? 第9輪Diff
比較:滿足條件【6】 oldCh
與 newCh
最后一個節(jié)點元素相同材蹬,執(zhí)行以下步驟, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??oldEIdx
位置減1吝镣,oldEnode
指向上一個節(jié)點>oldEnode=[--oldEIdx]
??newEIdx
位置減1堤器,newEnode
指向上一個節(jié)點>newEnode=[--newEIdx]
??第9輪Diff
比較結(jié)束后:
??oldCh
末尾記錄點oldEIdx
索引-1,由5
變?yōu)?code>4末贾,oldEnode
指向上一個節(jié)點吼旧,由E
變?yōu)?code>D。
??newCh
末尾記錄點newEIdx
索引-1未舟,由7
變?yōu)?code>6,newEnode
指向上一個節(jié)點掂为,由E
變?yōu)?code>I裕膀。
?? 看到這里,經(jīng)過以上每一輪的比較勇哗,基本上前 8 種
Diff
條件都已經(jīng)差不多全部執(zhí)行了昼扛,接下來的就是我們找出差異之后的DOM
更新了。
?? 第10輪Diff
比較:前 8 種Diff
條件都不滿足欲诺,則需要新增節(jié)點
?? 執(zhí)行以下步驟抄谐, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??parentElm
更新DOM
,將當(dāng)前的oldEnode
(E)元素插入到當(dāng)前oldSnode
(C)元素的前面
??newSIdx
位置+1扰法,newSnode
指向下一個節(jié)點>newSnode=[++newSIdx]
??第10輪Diff
比較結(jié)束后:
??parentElm
更新DOM
蛹含,將當(dāng)前的oldEnode
(E)元素插入到當(dāng)前oldSnode
(C)元素的前面
??newCh
初始記錄點newSIdx
索引+1,由3
變?yōu)?code>4塞颁,newSnode
指向下一個節(jié)點浦箱,由E
變?yōu)?code>M吸耿。
?? 第11輪Diff
比較:前 8 種Diff
條件都不滿足,則需要新增節(jié)點
?? 執(zhí)行以下步驟酷窥,根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??parentElm
更新DOM
咽安,將當(dāng)前的oldEnode
(M)元素插入到當(dāng)前oldSnode
(C)元素的前面
??newSIdx
位置+1,newSnode
指向下一個節(jié)點>newSnode=[++newSIdx]
??第11輪Diff
比較結(jié)束后:
??parentElm
更新DOM
蓬推,將當(dāng)前的oldEnode
(M)元素插入到當(dāng)前oldSnode
(C)元素的前面
??newCh
初始記錄點newSIdx
索引+1妆棒,由4
變?yōu)?code>5,newSnode
指向下一個節(jié)點沸伏,由M
變?yōu)?code>O糕珊。
;
?? 第12輪Diff
比較:前 8 種Diff
條件都不滿足,則需要新增節(jié)點
?? 執(zhí)行以下步驟馋评, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??parentElm
更新DOM
放接,將當(dāng)前的oldEnode
(O)元素插入到當(dāng)前oldSnode
(C)元素的前面
??newSIdx
位置+1,newSnode
指向下一個節(jié)點>newSnode=[++newSIdx]
??第12輪Diff
比較結(jié)束后:
??parentElm
更新DOM
留特,將當(dāng)前的oldEnode
(O)元素插入到當(dāng)前oldSnode
(C)元素的前面
??newCh
初始記錄點newSIdx
索引+1纠脾,由5
變?yōu)?code>6,newSnode
指向下一個節(jié)點蜕青,由M
變?yōu)?code>O苟蹈。
?? 第13輪Diff
比較:前 8 種Diff
條件都不滿足,則需要新增節(jié)點
?? 執(zhí)行以下步驟右核, 根據(jù)結(jié)果接著進行下一輪比較
??執(zhí)行:
??parentElm
更新DOM
慧脱,將當(dāng)前的oldEnode
(I)元素插入到當(dāng)前oldSnode
(C)元素的前面
??newSIdx
位置+1,newSnode
指向下一個節(jié)點>newSnode=[++newSIdx]
??第13輪Diff
比較結(jié)束后:
??parentElm
更新DOM
贺喝,將當(dāng)前的oldEnode
(I)元素插入到當(dāng)前oldSnode
(C)元素的前面
??newCh
初始記錄點newSIdx
索引+1菱鸥,由6
變?yōu)?code>7,newSnode
指向下一個節(jié)點躏鱼,由I
變?yōu)?code>E氮采。
??此時 newSIdx > newEIdx
,已經(jīng)不能滿足下一輪Diff
比較的條件染苛,Diff
比較比較結(jié)束
??經(jīng)過13輪的新舊子節(jié)點Diff
比較鹊漠,parentElm
通過將舊節(jié)點移動,新節(jié)點增加茶行,進行了DOM
的更新躯概,接下來就是根據(jù)最終oldCh
與newCh
剩余情況進行parentElm
的刪除或者新增。
??如果當(dāng)前newCh[newSIdx,newEIdx]
有剩余畔师,說明還有需要新增的元素娶靡,那么我們根據(jù)剩余的節(jié)點區(qū)間,依次插入到最終比較的newEnode
后面看锉。
??如果當(dāng)前oldCh[oldSIdx,oldEIdx]
有剩余固蛾,說明這些元素是需要刪除的结执,那么我們根據(jù)剩余的節(jié)點區(qū)間,依次刪除艾凯。
3.0 代碼模擬過程
基于Snabbdom
献幔,通過數(shù)組來表示DOM
節(jié)點元素,這里我稍微改造了一下趾诗,基于數(shù)組操作來模擬DOM
的更新
/**
* author:Echonessy
* des:
* date:2020.07.05
* target: Diff 算法模擬
* */
let oldCh = [null,"A","B","C","D","E","F","G",null];
let newCh = [null,"A","F","E","M","O","I","E","B","G",null];
// let oldCh = [null,"A","B","C","D","E","F","G",null];
// let newCh = [null,"A","F","C","D","E","M","O","I","E","B","G",null];
let parentElm = oldCh.slice();
diff(parentElm,oldCh,newCh);
// 判斷節(jié)點是否相同
export function same(vnode1, vnode2) {
return vnode1 === vnode2
}
// 模擬DOM節(jié)點插入與移動
function insertBerfore(parent,newNode,referenceNode,order) {
switch (order) {
case 'left':parent.splice(parent.indexOf(referenceNode),0,newNode);parent.splice(parent.indexOf(newNode.split('-')[1]),1);break;
case 'right':parent.splice(parent.indexOf(referenceNode),0,newNode);parent.splice(parent.lastIndexOf(newNode.split('-')[1]),1);break;
case 'diff':parent.splice(parent.indexOf(referenceNode.indexOf('-')!=-1?referenceNode.split('-')[1]:referenceNode),0,newNode);break;
case 'add':parent.splice(parent.lastIndexOf(referenceNode),0,newNode);break;
}
}
// 移除vNode
export function removeVnodes(parentElm,vnodes,startIdx,endIdx){
// 循環(huán)vnodes
for (; startIdx <= endIdx; ++startIdx) {
let ch = vnodes[startIdx];
if (ch != null) {
var index = parentElm.indexOf(ch);
parentElm.splice(index, 1)
}
}
}
// 添加vNode
function addVnodes(parentElm,before,vnodes,startIdx,endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
insertBerfore(parentElm,'new-'+ch,before,'add')
}
}
}
// diff 過程
function diff(parentElm,oldCh,newCh) {
console.log('初始')
console.log(oldCh)
console.log(newCh)
console.log(parentElm)
let oldSIdx = 0, newSIdx = 0;
let oldEIdx = oldCh.length - 1;
let oldSnode = oldCh[0];
let oldEnode = oldCh[oldEIdx];
let newEIdx = newCh.length - 1;
let newStnode = newCh[0];
let newEnode = newCh[newEIdx];
let conunt = 0; // 計數(shù)器蜡感,記錄循環(huán)次數(shù)
// 兩數(shù)組比較結(jié)束之前,循環(huán)調(diào)用Diff
while (oldSIdx <= oldEIdx && newSIdx <= newEIdx){
if (oldSnode == null) {
oldSnode = oldCh[++oldSIdx];
} else if (oldEnode == null) {
oldEnode = oldCh[--oldEIdx];
} else if (newStnode == null) {
newStnode = newCh[++newSIdx];
} else if (newEnode == null) {
newEnode = newCh[--newEIdx];
} else if (same(oldSnode, newStnode)){
oldSnode = oldCh[++oldSIdx];
newStnode = newCh[++newSIdx];
} else if (same(oldEnode, newEnode)) {
oldEnode = oldCh[--oldEIdx];
newEnode = newCh[--newEIdx];
}else if (same(oldSnode, newEnode)) {
insertBerfore(parentElm,'old-'+oldSnode,oldCh[oldEIdx+1],'left');
oldSnode = oldCh[++oldSIdx];
newEnode = newCh[--newEIdx];
}else if (same(oldEnode, newStnode)) {
insertBerfore(parentElm,'old-'+oldEnode,oldCh[oldSIdx],'right');
oldEnode = oldCh[--oldEIdx];
newStnode = newCh[++newSIdx];
} else {
insertBerfore(parentElm,'new-'+newStnode,oldCh[oldSIdx],'diff');
newStnode = newCh[++newSIdx];
}
console.log('------------------------------------------------------------------')
console.log('第'+ (++conunt) + '次對比,oldSIdx:'+oldSIdx + ' oldEIdx:' +oldEIdx + ' newSIdx:'+newSIdx+ ' newEIdx:'+newEIdx )
console.log(oldCh.slice(oldSIdx,oldEIdx+1))
console.log(newCh.slice(newSIdx,newEIdx+1))
console.log(parentElm);
}
// 最后比較完成之后
if (oldSIdx <= oldEIdx || newSIdx <= newEIdx) {
console.log('Diff 對比結(jié)束')
console.log(oldCh)
console.log(newCh)
if (oldSIdx > oldEIdx) {
// 'old-'用來模擬區(qū)分新舊節(jié)點
console.log('新增節(jié)點')
let before =newCh[newEIdx+1] == null ? null : 'old-'+ newCh[newEIdx+1];
addVnodes(parentElm, before, newCh, newSIdx, newEIdx);
} else {
console.log('移除節(jié)點')
removeVnodes(parentElm, oldCh, oldSIdx, oldEIdx);
}
console.log('模擬DOM更新完成')
console.log(parentElm);
}
}
4.0 附件
Snabbdom
源代碼地址:https://github.com/snabbdom/snabbdom