
最近複習到虛拟DOM與Diff,翻閱了衆多資料,特此總結了這篇長文,加深自己對vue的了解。
這篇文章比較詳細的分析了vue的虛拟DOM,Diff算法,其中一些關鍵的地方從别處搬運了一些圖進行說明(感謝制圖的大佬),也包含比較詳細的源碼解讀。
真實DOM的渲染
在講虛拟DOM之前,先說一下真實DOM的渲染。
浏覽器真實DOM渲染的過程大概分為以下幾個部分:
- 建構DOM樹。通過html parser解析處理html标記,将它們建構為DOM樹(DOM tree),當解析器遇到非阻塞資源(圖檔,css),會繼續解析,但是如果遇到script标簽(特别是沒有async 和 defer屬性),會阻塞渲染并停止html的解析,這就是為啥最好把script标簽放在body下面的原因。
- 建構cssOM樹。與建構DOM類似,浏覽器也會将樣式規則,建構成CSSOM。浏覽器會周遊CSS中的規則集,根據css選擇器建立具有父子,兄弟等關系的節點樹。
- 建構Render樹。這一步将DOM和CSSOM關聯,确定每個 DOM 元素應該應用什麼 CSS 規則。将所有相關樣式比對到DOM樹中的每個可見節點,并根據CSS級聯确定每個節點的計算樣式,不可見節點(head,屬性包括 display:none的節點)不會生成到Render樹中。
- 布局/回流(Layout/Reflow)。浏覽器第一次确定節點的位置以及大小叫布局,如果後續節點位置以及大小發生變化,這一步觸釋出局調整,也就是 回流。
- 繪制/重繪(Paint/Repaint)。将元素的每個可視部分繪制到螢幕上,包括文本、顔色、邊框、陰影和替換的元素(如按鈕和圖像)。如果文本、顔色、邊框、陰影等這些元素發生變化時,會觸發重繪(Repaint)。為了確定重繪的速度比初始繪制的速度更快,螢幕上的繪圖通常被分解成數層。将内容提升到GPU層(可以通過tranform,filter,will-change,opacity觸發)可以提高繪制以及重繪的性能。
- 合成(Compositing)。這一步将繪制過程中的分層合并,確定它們以正确的順序繪制到螢幕上顯示正确的内容。
為啥需要虛拟DOM
上面這是一次DOM渲染的過程,如果dom更新,那麼dom需要重新渲染一次,如果存在下面這種情況
<body>
<div id="container">
<div class="content" style="color: red;font-size:16px;">
This is a container
</div>
....
<div class="content" style="color: red;font-size:16px;">
This is a container
</div>
</div>
</body>
<script>
let content = document.getElementsByClassName('content');
for (let i = 0; i < 1000000; i++) {
content[i].innerHTML = `This is a content${i}`;
// 觸發回流
content[i].style.fontSize = `20px`;
}
</script>
那麼需要真實的操作DOM100w次,觸發了回流100w次。每次DOM的更新都會按照流程進行無差别的真實dom的更新。
是以造成了很大的性能浪費。如果循環裡面是複雜的操作,頻繁觸發回流與重繪,那麼就很容易就影響性能,造成卡頓。
另外這裡要說明一下的是,虛拟DOM并不是意味着比DOM就更快,性能需要分場景,虛拟DOM的性能跟模闆大小是正相關。
虛拟DOM的比較過程是不會區分資料量大小的,在元件内部隻有少量動态節點時,虛拟DOM依然是會對整個vdom進行周遊,相比直接渲染而言是多了一層操作的。
<div class="list">
<p class="item">item</p>
<p class="item">item</p>
<p class="item">item</p>
<p class="item">{{ item }}</p>
<p class="item">item</p>
<p class="item">item</p>
</div>
比如上面這個例子,虛拟DOM。雖然隻有一個動态節點,但是虛拟DOM依然需要周遊diff整個list的class,文本,标簽等資訊,最後依然需要進行DOM渲染。
如果隻是dom操作,就隻要操作一個具體的DOM然後進行渲染。
虛拟DOM最核心的價值在于,它能通過js描述真實DOM,表達力更強,通過聲明式的語言操作,為開發者提供了更加友善快捷開發體驗,而且在沒有手動優化,大部分情景下,保證了性能下限,成本效益更高。
虛拟DOM
虛拟DOM本質上是一個js對象,通過對象來表示真實的DOM結構。tag用來描述标簽,props用來描述屬性,children用來表示嵌套的層級關系。
const vnode = {
tag: 'div',
props: {
id: 'container',
},
children: [{
tag: 'div',
props: {
class: 'content',
},
text: 'This is a container'
}]
}
//對應的真實DOM結構
<div id="container">
<div class="content">
This is a container
</div>
</div>
虛拟DOM的更新不會立即操作DOM,而是會通過diff算法,找出需要更新的節點,按需更新,并将更新的内容儲存為一個js對象,更新完成後再挂載到真實dom上,實作真實的dom更新。
通過虛拟DOM,解決了操作真實DOM的三個問題。
- 無差别頻繁更新導緻DOM頻繁更新,造成性能問題
- 頻繁回流與重繪
- 開發體驗
另外由于虛拟DOM儲存的是js對象,天然的具有跨平台的能力,而不僅僅局限于浏覽器。
優點
總結起來,虛拟DOM的優勢有以下幾點:
- 小修改無需頻繁更新DOM,架構的diff算法會自動比較,分析出需要更新的節點,按需更新
- 更新資料不會造成頻繁的回流與重繪
- 表達力更強,資料更新更加友善
- 儲存的是js對象,具備跨平台能力
不足
虛拟DOM同樣也有缺點,首次渲染大量DOM時,由于多了一層虛拟DOM的計算,會比innerHTML插入慢。
虛拟DOM實作原理
主要分三部分:
- 通過js建立節點描述對象
- diff算法比較分析新舊兩個虛拟DOM差異
- 将差異patch到真實dom上實作更新
Diff算法
為了避免不必要的渲染,按需更新,虛拟DOM會采用Diff算法進行虛拟DOM節點比較,比較節點差異,進而确定需要更新的節點,再進行渲染。vue采用的是深度優先,同層比較的政策。
新節點與舊節點的比較主要是圍繞三件事來達到渲染目的
- 建立新節點
- 删除廢節點
- 更新已有節點
如何比較新舊節點是否一緻呢?
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節點的處理
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
//判斷兩個節點是否是同一種 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 相同或者兩個type都是text
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
可以看到,兩個節點是否相同是需要比較标簽(tag),屬性(在vue中是用data表示vnode中的屬性props), 注釋節點(isComment)的,另外碰到input的話,是會做特殊處理的。
建立新節點
當新節點有的,舊節點沒有,這就意味着這是全新的内容節點。隻有元素節點,文本節點,注釋節點才能被建立插入到DOM中。
删除舊節點
當舊節點有,而新節點沒有,那就意味着,新節點放棄了舊節點的一部分。删除節點會連帶的删除舊節點的子節點。
更新節點
新的節點與舊的的節點都有,那麼一切以新的為準,更新舊節點。如何判斷是否需要更新節點呢?
- 判斷新節點與舊節點是否完全一緻,一樣的話就不需要更新
// 判斷vnode與oldVnode是否完全一樣
if (oldVnode === vnode) {
return;
}
- 判斷新節點與舊節點是否是靜态節點,key是否一樣,是否是克隆節點(如果不是克隆節點,那麼意味着渲染函數被重置了,這個時候需要重新渲染)或者是否設定了once屬性,滿足條件的話替換componentInstance
// 是否是靜态節點,key是否一樣,是否是克隆節點或者是否設定了once屬性
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return;
}
- 判斷新節點是否有文本(通過text屬性判斷),如果有文本那麼需要比較同層級舊節點,如果舊節點文本不同于新節點文本,那麼采用新的文本内容。如果新節點沒有文本,那麼後面需要對子節點的相關情況進行判斷
//判斷新節點是否有文本
if (isUndef(vnode.text)) {
//如果沒有文本,處理子節點的相關代碼
....
} else if (oldVnode.text !== vnode.text) {
//新節點文本替換舊節點文本
nodeOps.setTextContent(elm, vnode.text)
}
- 判斷新節點與舊節點的子節點相關狀況。這裡又能分為4種情況
- 新節點與舊節點都有子節點
- 隻有新節點有子節點
- 隻有舊節點有子節點
- 新節點與舊節點都沒有子節點
都有子節點
對于都有子節點的情況,需要對新舊節點做比較,如果他們不相同,那麼需要進行diff操作,在vue中這裡就是updateChildren方法,後面會詳細再講,子節點的比較主要是雙端比較。
//判斷新節點是否有文本
if (isUndef(vnode.text)) {
//新舊節點都有子節點情況下,如果新舊子節點不相同,那麼進行子節點的比較,就是updateChildren方法
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
} else if (oldVnode.text !== vnode.text) {
//新節點文本替換舊節點文本
nodeOps.setTextContent(elm, vnode.text)
}
隻有新節點有子節點
隻有新節點有子節點,那麼就代表着這是新增的内容,那麼就是新增一個子節點到DOM,新增之前還會做一個重複key的檢測,并做出提醒,同時還要考慮,舊節點如果隻是一個文本節點,沒有子節點的情況,這種情況下就需要清空舊節點的文本内容。
//隻有新節點有子節點
if (isDef(ch)) {
//檢查重複key
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
//清除舊節點文本
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
//添加新節點
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}
//檢查重複key
function checkDuplicateKeys(children) {
const seenKeys = {}
for (let i = 0; i < children.length; i++) {
const vnode = children[i]
//子節點每一個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
}
}
}
}
隻有舊節點有子節點
隻有舊節點有,那就說明,新節點抛棄了舊節點的子節點,是以需要删除舊節點的子節點
if (isDef(oldCh)) {
//删除舊節點
removeVnodes(oldCh, 0, oldCh.length - 1)
}
都沒有子節點
這個時候需要對舊節點文本進行判斷,看舊節點是否有文本,如果有就清空
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)) {
// 克隆重用節點
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
}
// 是否是靜态節點,key是否一樣,是否是克隆節點或者是否設定了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)) {
//調用update回調以及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)
}
//判斷新節點是否有文本
if (isUndef(vnode.text)) {
//新舊節點都有子節點情況下,如果新舊子節點不相同,那麼進行子節點的比較,就是updateChildren方法
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
//隻有新節點有子節點
if (process.env.NODE_ENV !== 'production') {
//重複Key檢測
checkDuplicateKeys(ch)
}
//清除舊節點文本
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
//添加新節點
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
//隻有舊節點有子節點,删除舊節點
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
//新舊節點都無子節點
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
//新節點文本替換舊節點文本
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
配上流程圖會更清晰點
子節點的比較更新updateChildren
新舊節點都有子節點的情況下,這個時候是需要調用updateChildren方法來比較更新子節點的。是以在資料上,新舊節點子節點,就儲存為了兩個數組。
const oldCh = [oldVnode1, oldVnode2,oldVnode3];
const newCh = [newVnode1, newVnode2,newVnode3];
子節點更新采用的是雙端比較的政策,什麼是雙端比較呢,就是新舊節點比較是通過互相比較首尾元素(存在4種比較),然後向中間靠攏比較(newStartIdx,與oldStartIdx遞增,newEndIdx與oldEndIdx遞減)的政策。
比較過程
這裡對上面出現的新前,新後,舊前,舊後做一下說明
- 新前,指的是新節點未處理的子節點數組中的第一個元素,對應到vue源碼中的newStartVnode
- 新後,指的是新節點未處理的子節點數組中的最後一個元素,對應到vue源碼中的newEndVnode
- 舊前,指的是舊節點未處理的子節點數組中的第一個元素,對應到vue源碼中的oldStartVnode
- 舊後,指的是舊節點未處理的子節點數組中的最後一個元素,對應到vue源碼中的oldEndVnode
子節點比較過程
接下來對上面的比較過程以及比較後做的操作做下說明
- 新前與舊前的比較,如果他們相同,那麼進行上面說到的patchVnode更新操作,然後新舊節點各向後一步,進行第二項的比較...直到遇到不同才會換種比較方式
if (sameVnode(oldStartVnode, newStartVnode)) {
// 更新子節點
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 新舊各向後一步
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
- 新後與舊後的比較,如果他們相同,同樣進行pathchVnode更新,然後新舊各向前一步,進行前一項的比較...直到遇到不同,才會換比較方式
if (sameVnode(oldEndVnode, newEndVnode)) {
//更新子節點
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 新舊向前
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
- 新後與舊前的比較,如果它們相同,就進行更新操作,然後将舊前移動到所有未處理舊節點數組最後面,使舊前與新後位置保持一緻,然後雙方一起向中間靠攏,新向前,舊向後。如果不同會繼續切換比較方式
if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
//将舊子節點數組第一個子節點移動插入到最後
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
//舊向後
oldStartVnode = oldCh[++oldStartIdx]
//新向前
newEndVnode = newCh[--newEndIdx]
- 新前與舊後的比較,如果他們相同,就進行更新,然後将舊後移動到所有未處理舊節點數組最前面,是舊後與新前位置保持一緻,,然後新向後,舊向前,繼續向中間靠攏。繼續比較剩餘的節點。如果不同,就使用傳統的循環周遊查找。
if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
//将舊後移動插入到最前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
//舊向前
oldEndVnode = oldCh[--oldEndIdx]
//新向後
newStartVnode = newCh[++newStartIdx]
}
- 循環周遊查找,上面四種都沒找到的情況下,會通過key去查找比對。
進行到這一步對于沒有設定key的節點,第一次會通過createKeyToOldIdx建立key與index的映射 {key:index}
// 對于沒有設定key的節點,第一次會通過createKeyToOldIdx建立key與index的映射 {key:index}
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
然後拿新節點的key與舊節點進行比較,找到key值比對的節點的位置,這裡需要注意的是,如果新節點也沒key,那麼就會執行findIdxInOld方法,從頭到尾周遊比對舊節點。
//通過新節點的key,找到新節點在舊節點中所在的位置下标,如果沒有設定key,會執行周遊操作尋找
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]
//找到相同節點下标
if (isDef(c) && sameVnode(node, c)) return i
}
}
如果通過上面的方法,依舊沒找到新節點與舊節點比對的下标,那就說明這個節點是新節點,那就執行新增的操作。
//如果新節點無法在舊節點中找到自己的位置下标,說明是新元素,執行新增操作
if (isUndef(idxInOld)) {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
如果找到了,那麼說明在舊節點中找到了key值一樣,或者節點和key都一樣的舊節點。
如果節點一樣,那麼在patchVnode之後,需要将舊節點移動到所有未處理節點之前,對于key一樣,元素不同的節點,将其認為是新節點,執行新增操作。
執行完成後,新節點向後一步。
//如果新節點無法在舊節點中找到自己的位置下标,說明是新元素,執行新增操作
if (isUndef(idxInOld)) {
// 新增元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 在舊節點中找到了key值一樣的節點
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 相同子節點更新操作
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 更新完将舊節點指派undefined
oldCh[idxInOld] = undefined
//将舊節點移動到所有未處理節點之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 如果是相同的key,不同的元素,當做新節點,執行建立操作
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
//新節點向後
newStartVnode = newCh[++newStartIdx]
當完成對舊節點的周遊,但是新節點還沒完成周遊,那就說明後續的都是新增節點,執行新增操作,如果完成對新節點周遊,舊節點還沒完成周遊,那麼說明舊節點出現備援節點,執行删除操作。
//完成對舊節點的周遊,但是新節點還沒完成周遊,
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
// 新增節點
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 發現多餘的舊節點,執行删除操作
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
子節點比較總結
上面就是子節點比較更新的一個完整過程,這是完整的邏輯代碼。
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 <transition-group>
// 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)) {
//舊前向後移動
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 舊後向前移動
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
//新前與舊前
//更新子節點
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 新舊各向後一步
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
//新後與舊後
//更新子節點
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
//新舊各向前一步
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 新後與舊前
//更新子節點
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
//将舊前移動插入到最後
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)
//将舊後移動插入到最前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
//新向後,舊向前
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 對于沒有設定key的節點,第一次會通過createKeyToOldIdx建立key與index的映射 {key:index}
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
//通過新節點的key,找到新節點在舊節點中所在的位置下标,如果沒有設定key,會執行周遊操作尋找
idxInOld = isDef(newStartVnode.key) ?
oldKeyToIdx[newStartVnode.key] :
findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
//如果新節點無法在舊節點中找到自己的位置下标,說明是新元素,執行新增操作
if (isUndef(idxInOld)) {
// 新增元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 在舊節點中找到了key值一樣的節點
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 相同子節點更新操作
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 更新完将舊節點指派undefined
oldCh[idxInOld] = undefined
//将舊節點移動到所有未處理節點之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 如果是相同的key,不同的元素,當做新節點,執行建立操作
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
//新節點向後一步
newStartVnode = newCh[++newStartIdx]
}
}
//完成對舊節點的周遊,但是新節點還沒完成周遊,
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
// 新增節點
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 發現多餘的舊節點,執行删除操作
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
本文完~