首先介紹一下snabbdom,snabbdom是著名的虛擬DOM庫泻肯,是diff算法的鼻祖郭卫,Vue源碼借鑒了snabbdom。
1页藻、什么是虛擬DOM
虛擬DOM是用JavaScript對象描述DOM的層次結(jié)構(gòu)桨嫁。DOM中的一切屬性都在虛擬DOM中有對應(yīng)的屬性。本質(zhì)上是JS 和 DOM 之間的一個(gè)映射緩存份帐。虛擬DOM就是為了提高頁面渲染性能璃吧。
要點(diǎn):虛擬 DOM 是 JS 對象;虛擬 DOM 是對真實(shí) DOM 的描述废境。
為什么要使用虛擬DOM畜挨?
用JS對象模擬DOM節(jié)點(diǎn)的好處是筒繁,頁面的更新可以先全部反映在JS對象(虛擬DOM)上,操作內(nèi)存中的JS對象的速度顯然要更快巴元,等更新完成后毡咏,再將最終的JS對象映射成真實(shí)的DOM,交由瀏覽器去繪制逮刨。
diff發(fā)生在虛擬DOM上呕缭。diff算法是在新虛擬DOM和老虛擬DOM進(jìn)行diff(精細(xì)化比對),實(shí)現(xiàn)最小量更新修己,最后反映到真正的DOM上恢总。
1)最小量更新。key是vnode節(jié)點(diǎn)的唯一標(biāo)識睬愤,告訴了diff算法片仿,更改前后他們是同一個(gè)節(jié)點(diǎn)。
2)只有是同一個(gè)虛擬節(jié)點(diǎn)尤辱,才進(jìn)行精細(xì)化比較砂豌,否則就暴力刪除舊的、插入新的光督。
3)只進(jìn)行同層比較阳距,不會進(jìn)行跨層比較。
2可帽、h函數(shù)
我們前面知道diff算法發(fā)生在虛擬DOM上娄涩,而虛擬DOM是如何實(shí)現(xiàn)的呢?實(shí)際上虛擬DOM是有一個(gè)個(gè)虛擬節(jié)點(diǎn)組成映跟。
h函數(shù)用來產(chǎn)生虛擬節(jié)點(diǎn)(vnode)蓄拣。虛擬節(jié)點(diǎn)有如下的屬性:
1)sel: 標(biāo)簽類型,例如 p努隙、div球恤;
2)data: 標(biāo)簽上的數(shù)據(jù),例如 style荸镊、class咽斧、data-*;
3)children :子節(jié)點(diǎn)躬存;
4) text: 文本內(nèi)容张惹;
5)elm:虛擬節(jié)點(diǎn)綁定的真實(shí) DOM 節(jié)點(diǎn);
通過h函數(shù)的嵌套岭洲,從而得到虛擬DOM樹宛逗。
?我們編寫了一個(gè)低配版的h函數(shù),必須傳入3個(gè)參數(shù)盾剩,重載較弱雷激。
?*?形態(tài)1:h('div',?{},?'文字')
?*?形態(tài)2:h('div',?{},?[])
?*?形態(tài)3:h('div',?{},?h())
首先定義vnode節(jié)點(diǎn)替蔬,實(shí)際上就是把傳入的參數(shù)合成對象返回。
然后編寫h函數(shù)屎暇,根據(jù)第三個(gè)參數(shù)的不同進(jìn)行不同的響應(yīng)承桥。
3、patch函數(shù)
如何定義是同一個(gè)節(jié)點(diǎn)呢根悼?舊節(jié)點(diǎn)的key要和新節(jié)點(diǎn)的key相同且選擇器也相同凶异。
當(dāng)調(diào)用patch函數(shù)時(shí),會傳入舊番挺、新節(jié)點(diǎn)唠帝。首先我們判斷舊節(jié)點(diǎn)oldVnode是否是虛擬節(jié)點(diǎn)屯掖,如果傳入的不是虛擬節(jié)點(diǎn)是DOM節(jié)點(diǎn)玄柏,我們需要進(jìn)行包裝成虛擬節(jié)點(diǎn),即調(diào)用vnode函數(shù)進(jìn)行轉(zhuǎn)化贴铜。
然后判斷oldVnode和newVnode是不是同一個(gè)節(jié)點(diǎn)粪摘,如果是則進(jìn)行精細(xì)化比較(這個(gè)后面再完成);若不是绍坝,則將新節(jié)點(diǎn)創(chuàng)建為DOM徘意,然后添加到頁面中,并移除舊節(jié)點(diǎn)轩褐。
createElement函數(shù)用來創(chuàng)建節(jié)點(diǎn)椎咧,將vnode節(jié)點(diǎn)創(chuàng)建為DOM。若vnode節(jié)點(diǎn)中存在嵌套把介,我們需要遞歸調(diào)用createElement完成子節(jié)點(diǎn)的創(chuàng)建勤讽。若為文本或undefined,則為直接轉(zhuǎn)換為DOM拗踢。
如果oldVnode和newVnode是同一個(gè)節(jié)點(diǎn)脚牍,我們需要繼續(xù)進(jìn)行判斷比較。首先判斷oldVnode和newVnode是同一個(gè)對象巢墅,是則什么都不做诸狭,若不是,
判斷newVnode有沒有text屬性君纫,有則判斷text相不相同驯遇,不同則用新的text屬性代替;
若newVnode沒有text屬性蓄髓,意味著有newVnode有children叉庐,再判斷oldVnode有沒有children,沒有(意味著有oldVnode有text)双吆,清空oldVnode的text眨唬,將newVnode的children添加到DOM中会前;
若oldVnode有children,則需要進(jìn)行diff了(后面再續(xù))匾竿。
4瓦宜、diff算法
當(dāng)我們進(jìn)行比較的過程中,我們采用的4種命中查找策略:
1)新前與舊前:命中則指針同時(shí)往后移動岭妖。
2)新后與舊后:命中則指針同時(shí)往前移動临庇。
3)新后與舊前:命中則涉及節(jié)點(diǎn)移動,那么新后指向的節(jié)點(diǎn)昵慌,移到舊后之后假夺。
4)新前與舊后:命中則涉及節(jié)點(diǎn)移動,那么新前指向的節(jié)點(diǎn)斋攀,移到舊前之前已卷。
命中上述4種一種就不在命中判斷了,如果沒有命中淳蔼,就需要循環(huán)來尋找侧蘸,移動到舊前之前。直到while(新前<=新后&&舊前<=就后)不成立則完成鹉梨。
如果是新節(jié)點(diǎn)先循環(huán)完畢讳癌,如果老節(jié)點(diǎn)中還有剩余節(jié)點(diǎn)(舊前和舊后指針中間的節(jié)點(diǎn)),說明他們是要被刪除的節(jié)點(diǎn)存皂。
如果是舊節(jié)點(diǎn)先循環(huán)完畢晌坤,說明新節(jié)點(diǎn)中有要插入的節(jié)點(diǎn)。
當(dāng)新老VNode節(jié)點(diǎn)的start相同時(shí)旦袋,直接patchVnode骤菠,同時(shí)新老VNode節(jié)點(diǎn)的開始索引都加 1
當(dāng)新老VNode節(jié)點(diǎn)的end相同時(shí),同樣直接patchVnode猜憎,同時(shí)新老VNode節(jié)點(diǎn)的結(jié)束索引都減 1
當(dāng)老VNode節(jié)點(diǎn)的start和新VNode節(jié)點(diǎn)的end相同時(shí)娩怎,這時(shí)候在patchVnode后,還需要將當(dāng)前真實(shí)dom節(jié)點(diǎn)移動到oldEndVnode的后面胰柑,同時(shí)老VNode節(jié)點(diǎn)開始索引加 1截亦,新VNode節(jié)點(diǎn)的結(jié)束索引減 1
當(dāng)老VNode節(jié)點(diǎn)的end和新VNode節(jié)點(diǎn)的start相同時(shí),這時(shí)候在patchVnode后柬讨,還需要將當(dāng)前真實(shí)dom節(jié)點(diǎn)移動到oldStartVnode的前面崩瓤,同時(shí)老VNode節(jié)點(diǎn)結(jié)束索引減 1,新VNode節(jié)點(diǎn)的開始索引加 1
如果都不滿足以上四種情形踩官,那說明沒有相同的節(jié)點(diǎn)可以復(fù)用却桶,則會分為以下兩種情況:
從舊的VNode為key值,對應(yīng)index序列為value值的哈希表中找到與newStartVnode一致key的舊的VNode節(jié)點(diǎn),再進(jìn)行patchVnode颖系,同時(shí)將這個(gè)真實(shí)dom移動到oldStartVnode對應(yīng)的真實(shí)dom的前面
調(diào)用createElm創(chuàng)建一個(gè)新的dom節(jié)點(diǎn)放到當(dāng)前newStartIdx的位置嗅剖。