怎樣深入理解vue中的虛擬DOM和Diff算法,針對這個(gè)問題,這篇文章詳細(xì)介紹了相對應(yīng)的分析和解答,希望可以幫助更多想解決這個(gè)問題的小伙伴找到更簡單易行的方法。
創(chuàng)新互聯(lián)建站是一家專注于成都網(wǎng)站制作、成都網(wǎng)站建設(shè)、外貿(mào)營銷網(wǎng)站建設(shè)與策劃設(shè)計(jì),鄂州網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)建站做網(wǎng)站,專注于網(wǎng)站建設(shè)10余年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:鄂州等地區(qū)。鄂州做網(wǎng)站價(jià)格咨詢:13518219792
在講虛擬DOM之前,先說一下真實(shí)DOM的渲染。
瀏覽器真實(shí)DOM渲染的過程大概分為以下幾個(gè)部分
構(gòu)建DOM樹。通過HTML parser解析處理HTML標(biāo)記,將它們構(gòu)建為DOM樹(DOM tree),當(dāng)解析器遇到非阻塞資源(圖片,css),會(huì)繼續(xù)解析,但是如果遇到script標(biāo)簽(特別是沒有async 和 defer屬性),會(huì)阻塞渲染并停止html的解析,這就是為啥最好把script標(biāo)簽放在body下面的原因。
構(gòu)建CSSOM樹。與構(gòu)建DOM類似,瀏覽器也會(huì)將樣式規(guī)則,構(gòu)建成CSSOM。瀏覽器會(huì)遍歷CSS中的規(guī)則集,根據(jù)css選擇器創(chuàng)建具有父子,兄弟等關(guān)系的節(jié)點(diǎn)樹。
構(gòu)建Render樹。這一步將DOM和CSSOM關(guān)聯(lián),確定每個(gè) DOM 元素應(yīng)該應(yīng)用什么 CSS 規(guī)則。將所有相關(guān)樣式匹配到DOM樹中的每個(gè)可見節(jié)點(diǎn),并根據(jù)CSS級聯(lián)確定每個(gè)節(jié)點(diǎn)的計(jì)算樣式,不可見節(jié)點(diǎn)(head,屬性包括 display:none的節(jié)點(diǎn))不會(huì)生成到Render樹中。
布局/回流(Layout/Reflow)。瀏覽器第一次確定節(jié)點(diǎn)的位置以及大小叫布局,如果后續(xù)節(jié)點(diǎn)位置以及大小發(fā)生變化,這一步觸發(fā)布局調(diào)整,也就是 回流。
繪制/重繪(Paint/Repaint)。將元素的每個(gè)可視部分繪制到屏幕上,包括文本、顏色、邊框、陰影和替換的元素(如按鈕和圖像)。如果文本、顏色、邊框、陰影等這些元素發(fā)生變化時(shí),會(huì)觸發(fā)重繪(Repaint)。為了確保重繪的速度比初始繪制的速度更快,屏幕上的繪圖通常被分解成數(shù)層。將內(nèi)容提升到GPU層(可以通過tranform,filter,will-change,opacity觸發(fā))可以提高繪制以及重繪的性能。
合成(Compositing)。這一步將繪制過程中的分層合并,確保它們以正確的順序繪制到屏幕上顯示正確的內(nèi)容。
上面這是一次DOM渲染的過程,如果dom更新,那么dom需要重新渲染一次,如果存在下面這種情況
This is a container
那么需要真實(shí)的操作DOM100w次,觸發(fā)了回流100w次。每次DOM的更新都會(huì)按照流程進(jìn)行無差別的真實(shí)dom的更新。所以造成了很大的性能浪費(fèi)。如果循環(huán)里面是復(fù)雜的操作,頻繁觸發(fā)回流與重繪,那么就很容易就影響性能,造成卡頓。另外這里要說明一下的是,虛擬DOM并不是意味著比DOM就更快,性能需要分場景,虛擬DOM的性能跟模板大小是正相關(guān)。虛擬DOM的比較過程是不會(huì)區(qū)分?jǐn)?shù)據(jù)量大小的,在組件內(nèi)部只有少量動(dòng)態(tài)節(jié)點(diǎn)時(shí),虛擬DOM依然是會(huì)對整個(gè)vdom進(jìn)行遍歷,相比直接渲染而言是多了一層操作的。
item
item
item
{{ item }}
item
item
比如上面這個(gè)例子,虛擬DOM。雖然只有一個(gè)動(dòng)態(tài)節(jié)點(diǎn),但是虛擬DOM依然需要遍歷diff整個(gè)list的class,文本,標(biāo)簽等信息,最后依然需要進(jìn)行DOM渲染。如果只是dom操作,就只要操作一個(gè)具體的DOM然后進(jìn)行渲染。虛擬DOM最核心的價(jià)值在于,它能通過js描述真實(shí)DOM,表達(dá)力更強(qiáng),通過聲明式的語言操作,為開發(fā)者提供了更加方便快捷開發(fā)體驗(yàn),而且在沒有手動(dòng)優(yōu)化,大部分情景下,保證了性能下限,性價(jià)比更高。
虛擬DOM本質(zhì)上是一個(gè)js對象,通過對象來表示真實(shí)的DOM結(jié)構(gòu)。tag用來描述標(biāo)簽,props用來描述屬性,children用來表示嵌套的層級關(guān)系。
const vnode = { tag: 'div', props: { id: 'container', }, children: [{ tag: 'div', props: { class: 'content', }, text: 'This is a container' }] } //對應(yīng)的真實(shí)DOM結(jié)構(gòu)This is a container
虛擬DOM的更新不會(huì)立即操作DOM,而是會(huì)通過diff算法,找出需要更新的節(jié)點(diǎn),按需更新,并將更新的內(nèi)容保存為一個(gè)js對象,更新完成后再掛載到真實(shí)dom上,實(shí)現(xiàn)真實(shí)的dom更新。通過虛擬DOM,解決了操作真實(shí)DOM的三個(gè)問題。
無差別頻繁更新導(dǎo)致DOM頻繁更新,造成性能問題
頻繁回流與重繪
開發(fā)體驗(yàn)
另外由于虛擬DOM保存的是js對象,天然的具有跨平臺的能力,而不僅僅局限于瀏覽器。
優(yōu)點(diǎn)
總結(jié)起來,虛擬DOM的優(yōu)勢有以下幾點(diǎn)
小修改無需頻繁更新DOM,框架的diff算法會(huì)自動(dòng)比較,分析出需要更新的節(jié)點(diǎn),按需更新
更新數(shù)據(jù)不會(huì)造成頻繁的回流與重繪
表達(dá)力更強(qiáng),數(shù)據(jù)更新更加方便
保存的是js對象,具備跨平臺能力
不足
虛擬DOM同樣也有缺點(diǎn),首次渲染大量DOM時(shí),由于多了一層虛擬DOM的計(jì)算,會(huì)比innerHTML插入慢。
主要分三部分
通過js建立節(jié)點(diǎn)描述對象
diff算法比較分析新舊兩個(gè)虛擬DOM差異
將差異patch到真實(shí)dom上實(shí)現(xiàn)更新
Diff算法
為了避免不必要的渲染,按需更新,虛擬DOM會(huì)采用Diff算法進(jìn)行虛擬DOM節(jié)點(diǎn)比較,比較節(jié)點(diǎn)差異,從而確定需要更新的節(jié)點(diǎn),再進(jìn)行渲染。vue采用的是深度優(yōu)先,同層比較的策略。
新節(jié)點(diǎn)與舊節(jié)點(diǎn)的比較主要是圍繞三件事來達(dá)到渲染目的
創(chuàng)建新節(jié)點(diǎn)
刪除廢節(jié)點(diǎn)
更新已有節(jié)點(diǎn)
如何比較新舊節(jié)點(diǎn)是否一致呢?
function sameVnode(a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) //對input節(jié)點(diǎn)的處理 ) || ( isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error) ) ) ) } //判斷兩個(gè)節(jié)點(diǎn)是否是同一種 input 輸入類型 function sameInputType(a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type //input type 相同或者兩個(gè)type都是text return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) }
可以看到,兩個(gè)節(jié)點(diǎn)是否相同是需要比較標(biāo)簽(tag),屬性(在vue中是用data表示vnode中的屬性props), 注釋節(jié)點(diǎn)(isComment)的,另外碰到input的話,是會(huì)做特殊處理的。
創(chuàng)建新節(jié)點(diǎn)
當(dāng)新節(jié)點(diǎn)有的,舊節(jié)點(diǎn)沒有,這就意味著這是全新的內(nèi)容節(jié)點(diǎn)。只有元素節(jié)點(diǎn),文本節(jié)點(diǎn),注釋節(jié)點(diǎn)才能被創(chuàng)建插入到DOM中。
刪除舊節(jié)點(diǎn)
當(dāng)舊節(jié)點(diǎn)有,而新節(jié)點(diǎn)沒有,那就意味著,新節(jié)點(diǎn)放棄了舊節(jié)點(diǎn)的一部分。刪除節(jié)點(diǎn)會(huì)連帶的刪除舊節(jié)點(diǎn)的子節(jié)點(diǎn)。
更新節(jié)點(diǎn)
新的節(jié)點(diǎn)與舊的的節(jié)點(diǎn)都有,那么一切以新的為準(zhǔn),更新舊節(jié)點(diǎn)。如何判斷是否需要更新節(jié)點(diǎn)呢?
判斷新節(jié)點(diǎn)與舊節(jié)點(diǎn)是否完全一致,一樣的話就不需要更新
// 判斷vnode與oldVnode是否完全一樣 if (oldVnode === vnode) { return; }
判斷新節(jié)點(diǎn)與舊節(jié)點(diǎn)是否是靜態(tài)節(jié)點(diǎn),key是否一樣,是否是克隆節(jié)點(diǎn)(如果不是克隆節(jié)點(diǎn),那么意味著渲染函數(shù)被重置了,這個(gè)時(shí)候需要重新渲染)或者是否設(shè)置了once屬性,滿足條件的話替換componentInstance
// 是否是靜態(tài)節(jié)點(diǎn),key是否一樣,是否是克隆節(jié)點(diǎn)或者是否設(shè)置了once屬性 if ( isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance; return; }
判斷新節(jié)點(diǎn)是否有文本(通過text屬性判斷),如果有文本那么需要比較同層級舊節(jié)點(diǎn),如果舊節(jié)點(diǎn)文本不同于新節(jié)點(diǎn)文本,那么采用新的文本內(nèi)容。如果新節(jié)點(diǎn)沒有文本,那么后面需要對子節(jié)點(diǎn)的相關(guān)情況進(jìn)行判斷
//判斷新節(jié)點(diǎn)是否有文本 if (isUndef(vnode.text)) { //如果沒有文本,處理子節(jié)點(diǎn)的相關(guān)代碼 .... } else if (oldVnode.text !== vnode.text) { //新節(jié)點(diǎn)文本替換舊節(jié)點(diǎn)文本 nodeOps.setTextContent(elm, vnode.text) }
判斷新節(jié)點(diǎn)與舊節(jié)點(diǎn)的子節(jié)點(diǎn)相關(guān)狀況。這里又能分為4種情況
新節(jié)點(diǎn)與舊節(jié)點(diǎn)都有子節(jié)點(diǎn)
只有新節(jié)點(diǎn)有子節(jié)點(diǎn)
只有舊節(jié)點(diǎn)有子節(jié)點(diǎn)
新節(jié)點(diǎn)與舊節(jié)點(diǎn)都沒有子節(jié)點(diǎn)
都有子節(jié)點(diǎn)
對于都有子節(jié)點(diǎn)的情況,需要對新舊節(jié)點(diǎn)做比較,如果他們不相同,那么需要進(jìn)行diff操作,在vue中這里就是updateChildren方法,后面會(huì)詳細(xì)再講,子節(jié)點(diǎn)的比較主要是雙端比較。
//判斷新節(jié)點(diǎn)是否有文本 if (isUndef(vnode.text)) { //新舊節(jié)點(diǎn)都有子節(jié)點(diǎn)情況下,如果新舊子節(jié)點(diǎn)不相同,那么進(jìn)行子節(jié)點(diǎn)的比較,就是updateChildren方法 if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } } else if (oldVnode.text !== vnode.text) { //新節(jié)點(diǎn)文本替換舊節(jié)點(diǎn)文本 nodeOps.setTextContent(elm, vnode.text) }
只有新節(jié)點(diǎn)有子節(jié)點(diǎn)
只有新節(jié)點(diǎn)有子節(jié)點(diǎn),那么就代表著這是新增的內(nèi)容,那么就是新增一個(gè)子節(jié)點(diǎn)到DOM,新增之前還會(huì)做一個(gè)重復(fù)key的檢測,并做出提醒,同時(shí)還要考慮,舊節(jié)點(diǎn)如果只是一個(gè)文本節(jié)點(diǎn),沒有子節(jié)點(diǎn)的情況,這種情況下就需要清空舊節(jié)點(diǎn)的文本內(nèi)容。
//只有新節(jié)點(diǎn)有子節(jié)點(diǎn) if (isDef(ch)) { //檢查重復(fù)key if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } //清除舊節(jié)點(diǎn)文本 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') //添加新節(jié)點(diǎn) addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } //檢查重復(fù)key function checkDuplicateKeys(children) { const seenKeys = {} for (let i = 0; i < children.length; i++) { const vnode = children[i] //子節(jié)點(diǎn)每一個(gè)Key const key = vnode.key if (isDef(key)) { if (seenKeys[key]) { warn( `Duplicate keys detected: '${key}'. This may cause an update error.`, vnode.context ) } else { seenKeys[key] = true } } } }
只有舊節(jié)點(diǎn)有子節(jié)點(diǎn)
只有舊節(jié)點(diǎn)有,那就說明,新節(jié)點(diǎn)拋棄了舊節(jié)點(diǎn)的子節(jié)點(diǎn),所以需要?jiǎng)h除舊節(jié)點(diǎn)的子節(jié)點(diǎn)
if (isDef(oldCh)) { //刪除舊節(jié)點(diǎn) removeVnodes(oldCh, 0, oldCh.length - 1) }
都沒有子節(jié)點(diǎn)
這個(gè)時(shí)候需要對舊節(jié)點(diǎn)文本進(jìn)行判斷,看舊節(jié)點(diǎn)是否有文本,如果有就清空
if (isDef(oldVnode.text)) { //清空 nodeOps.setTextContent(elm, '') }
整體的邏輯代碼如下
function patchVnode( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 判斷vnode與oldVnode是否完全一樣 if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // 克隆重用節(jié)點(diǎn) vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 是否是靜態(tài)節(jié)點(diǎn),key是否一樣,是否是克隆節(jié)點(diǎn)或者是否設(shè)置了once屬性 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { //調(diào)用update回調(diào)以及update鉤子 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) } //判斷新節(jié)點(diǎn)是否有文本 if (isUndef(vnode.text)) { //新舊節(jié)點(diǎn)都有子節(jié)點(diǎn)情況下,如果新舊子節(jié)點(diǎn)不相同,那么進(jìn)行子節(jié)點(diǎn)的比較,就是updateChildren方法 if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { //只有新節(jié)點(diǎn)有子節(jié)點(diǎn) if (process.env.NODE_ENV !== 'production') { //重復(fù)Key檢測 checkDuplicateKeys(ch) } //清除舊節(jié)點(diǎn)文本 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') //添加新節(jié)點(diǎn) addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { //只有舊節(jié)點(diǎn)有子節(jié)點(diǎn),刪除舊節(jié)點(diǎn) removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { //新舊節(jié)點(diǎn)都無子節(jié)點(diǎn) nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { //新節(jié)點(diǎn)文本替換舊節(jié)點(diǎn)文本 nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
配上流程圖會(huì)更清晰點(diǎn)
子節(jié)點(diǎn)的比較更新updateChildren
新舊節(jié)點(diǎn)都有子節(jié)點(diǎn)的情況下,這個(gè)時(shí)候是需要調(diào)用updateChildren方法來比較更新子節(jié)點(diǎn)的。所以在數(shù)據(jù)上,新舊節(jié)點(diǎn)子節(jié)點(diǎn),就保存為了兩個(gè)數(shù)組。
const oldCh = [oldVnode1, oldVnode2,oldVnode3]; const newCh = [newVnode1, newVnode2,newVnode3];
子節(jié)點(diǎn)更新采用的是雙端比較的策略,什么是雙端比較呢,就是新舊節(jié)點(diǎn)比較是通過互相比較首尾元素(存在4種比較),然后向中間靠攏比較(newStartIdx,與oldStartIdx遞增,newEndIdx與oldEndIdx遞減)的策略。
比較過程
向中間靠攏
這里對上面出現(xiàn)的新前,新后,舊前,舊后做一下說明
新前,指的是新節(jié)點(diǎn)未處理的子節(jié)點(diǎn)數(shù)組中的第一個(gè)元素,對應(yīng)到vue源碼中的newStartVnode
新后,指的是新節(jié)點(diǎn)未處理的子節(jié)點(diǎn)數(shù)組中的最后一個(gè)元素,對應(yīng)到vue源碼中的newEndVnode
舊前,指的是舊節(jié)點(diǎn)未處理的子節(jié)點(diǎn)數(shù)組中的第一個(gè)元素,對應(yīng)到vue源碼中的oldStartVnode
舊后,指的是舊節(jié)點(diǎn)未處理的子節(jié)點(diǎn)數(shù)組中的最后一個(gè)元素,對應(yīng)到vue源碼中的oldEndVnode
子節(jié)點(diǎn)比較過程
接下來對上面的比較過程以及比較后做的操作做下說明
新前與舊前的比較,如果他們相同,那么進(jìn)行上面說到的patchVnode更新操作,然后新舊節(jié)點(diǎn)各向后一步,進(jìn)行第二項(xiàng)的比較...直到遇到不同才會(huì)換種比較方式
if (sameVnode(oldStartVnode, newStartVnode)) { // 更新子節(jié)點(diǎn) patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 新舊各向后一步 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }
新后與舊后的比較,如果他們相同,同樣進(jìn)行pathchVnode更新,然后新舊各向前一步,進(jìn)行前一項(xiàng)的比較...直到遇到不同,才會(huì)換比較方式
if (sameVnode(oldEndVnode, newEndVnode)) { //更新子節(jié)點(diǎn) patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // 新舊向前 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }
新后與舊前的比較,如果它們相同,就進(jìn)行更新操作,然后將舊前移動(dòng)到所有未處理舊節(jié)點(diǎn)數(shù)組最后面,使舊前與新后位置保持一致,然后雙方一起向中間靠攏,新向前,舊向后。如果不同會(huì)繼續(xù)切換比較方式
if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) //將舊子節(jié)點(diǎn)數(shù)組第一個(gè)子節(jié)點(diǎn)移動(dòng)插入到最后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) //舊向后 oldStartVnode = oldCh[++oldStartIdx] //新向前 newEndVnode = newCh[--newEndIdx]
新前與舊后的比較,如果他們相同,就進(jìn)行更新,然后將舊后移動(dòng)到所有未處理舊節(jié)點(diǎn)數(shù)組最前面,是舊后與新前位置保持一致,,然后新向后,舊向前,繼續(xù)向中間靠攏。繼續(xù)比較剩余的節(jié)點(diǎn)。如果不同,就使用傳統(tǒng)的循環(huán)遍歷查找。
if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) //將舊后移動(dòng)插入到最前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) //舊向前 oldEndVnode = oldCh[--oldEndIdx] //新向后 newStartVnode = newCh[++newStartIdx] }
循環(huán)遍歷查找,上面四種都沒找到的情況下,會(huì)通過key去查找匹配。
進(jìn)行到這一步對于沒有設(shè)置key的節(jié)點(diǎn),第一次會(huì)通過createKeyToOldIdx建立key與index的映射 {key:index}
// 對于沒有設(shè)置key的節(jié)點(diǎn),第一次會(huì)通過createKeyToOldIdx建立key與index的映射 {key:index} if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
然后拿新節(jié)點(diǎn)的key與舊節(jié)點(diǎn)進(jìn)行比較,找到key值匹配的節(jié)點(diǎn)的位置,這里需要注意的是,如果新節(jié)點(diǎn)也沒key,那么就會(huì)執(zhí)行findIdxInOld方法,從頭到尾遍歷匹配舊節(jié)點(diǎn)
//通過新節(jié)點(diǎn)的key,找到新節(jié)點(diǎn)在舊節(jié)點(diǎn)中所在的位置下標(biāo),如果沒有設(shè)置key,會(huì)執(zhí)行遍歷操作尋找 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) //findIdxInOld方法 function findIdxInOld(node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] //找到相同節(jié)點(diǎn)下標(biāo) if (isDef(c) && sameVnode(node, c)) return i } }
如果通過上面的方法,依舊沒找到新節(jié)點(diǎn)與舊節(jié)點(diǎn)匹配的下標(biāo),那就說明這個(gè)節(jié)點(diǎn)是新節(jié)點(diǎn),那就執(zhí)行新增的操作。
//如果新節(jié)點(diǎn)無法在舊節(jié)點(diǎn)中找到自己的位置下標(biāo),說明是新元素,執(zhí)行新增操作 if (isUndef(idxInOld)) { createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) }
如果找到了,那么說明在舊節(jié)點(diǎn)中找到了key值一樣,或者節(jié)點(diǎn)和key都一樣的舊節(jié)點(diǎn)。如果節(jié)點(diǎn)一樣,那么在patchVnode之后,需要將舊節(jié)點(diǎn)移動(dòng)到所有未處理節(jié)點(diǎn)之前,對于key一樣,元素不同的節(jié)點(diǎn),將其認(rèn)為是新節(jié)點(diǎn),執(zhí)行新增操作。執(zhí)行完成后,新節(jié)點(diǎn)向后一步。
//如果新節(jié)點(diǎn)無法在舊節(jié)點(diǎn)中找到自己的位置下標(biāo),說明是新元素,執(zhí)行新增操作 if (isUndef(idxInOld)) { // 新增元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 在舊節(jié)點(diǎn)中找到了key值一樣的節(jié)點(diǎn) vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // 相同子節(jié)點(diǎn)更新操作 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 更新完將舊節(jié)點(diǎn)賦值undefined oldCh[idxInOld] = undefined //將舊節(jié)點(diǎn)移動(dòng)到所有未處理節(jié)點(diǎn)之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果是相同的key,不同的元素,當(dāng)做新節(jié)點(diǎn),執(zhí)行創(chuàng)建操作 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } //新節(jié)點(diǎn)向后 newStartVnode = newCh[++newStartIdx]
當(dāng)完成對舊節(jié)點(diǎn)的遍歷,但是新節(jié)點(diǎn)還沒完成遍歷,那就說明后續(xù)的都是新增節(jié)點(diǎn),執(zhí)行新增操作,如果完成對新節(jié)點(diǎn)遍歷,舊節(jié)點(diǎn)還沒完成遍歷,那么說明舊節(jié)點(diǎn)出現(xiàn)冗余節(jié)點(diǎn),執(zhí)行刪除操作。
//完成對舊節(jié)點(diǎn)的遍歷,但是新節(jié)點(diǎn)還沒完成遍歷, if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm // 新增節(jié)點(diǎn) addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 發(fā)現(xiàn)多余的舊節(jié)點(diǎn),執(zhí)行刪除操作 removeVnodes(oldCh, oldStartIdx, oldEndIdx) }
子節(jié)點(diǎn)比較總結(jié)
上面就是子節(jié)點(diǎn)比較更新的一個(gè)完整過程,這是完整的邏輯代碼
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { 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] //新后 let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly is a special flag used only by// to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } //雙端比較遍歷 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { //舊前向后移動(dòng) oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { // 舊后向前移動(dòng) oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { //新前與舊前 //更新子節(jié)點(diǎn) patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 新舊各向后一步 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { //新后與舊后 //更新子節(jié)點(diǎn) patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) //新舊各向前一步 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // 新后與舊前 //更新子節(jié)點(diǎn) patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) //將舊前移動(dòng)插入到最后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) //新向前,舊向后 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // 新前與舊后 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) //將舊后移動(dòng)插入到最前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) //新向后,舊向前 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 對于沒有設(shè)置key的節(jié)點(diǎn),第一次會(huì)通過createKeyToOldIdx建立key與index的映射 {key:index} if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) //通過新節(jié)點(diǎn)的key,找到新節(jié)點(diǎn)在舊節(jié)點(diǎn)中所在的位置下標(biāo),如果沒有設(shè)置key,會(huì)執(zhí)行遍歷操作尋找 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) //如果新節(jié)點(diǎn)無法在舊節(jié)點(diǎn)中找到自己的位置下標(biāo),說明是新元素,執(zhí)行新增操作 if (isUndef(idxInOld)) { // 新增元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 在舊節(jié)點(diǎn)中找到了key值一樣的節(jié)點(diǎn) vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // 相同子節(jié)點(diǎn)更新操作 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 更新完將舊節(jié)點(diǎn)賦值undefined oldCh[idxInOld] = undefined //將舊節(jié)點(diǎn)移動(dòng)到所有未處理節(jié)點(diǎn)之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果是相同的key,不同的元素,當(dāng)做新節(jié)點(diǎn),執(zhí)行創(chuàng)建操作 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } //新節(jié)點(diǎn)向后一步 newStartVnode = newCh[++newStartIdx] } } //完成對舊節(jié)點(diǎn)的遍歷,但是新節(jié)點(diǎn)還沒完成遍歷, if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm // 新增節(jié)點(diǎn) addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 發(fā)現(xiàn)多余的舊節(jié)點(diǎn),執(zhí)行刪除操作 removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }
關(guān)于怎樣深入理解vue中的虛擬DOM和Diff算法問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道了解更多相關(guān)知識。