目錄
9.1 減少 DOM 操作的性能開銷
9.2 DOM 復用與 key 的作用
9.3 找到需要移動的元素
9.4 如何移動元素
9.5 添加新元素
9.6 移除不存在的元素
9.7 總結
當新舊 vnode 的子節點都是一組節點時,為了以最小的性能開銷完成更新操作,需要比較兩組子節點,用于比較的算法就叫作 Diff 算法。
9.1 減少 DOM 操作的性能開銷
之前我們在更新子節點時,簡單地移除所有舊的子節點,然后添加所有新的子節點。
這種方式雖然簡單直接,但會產生大量的性能開銷,因為它沒有復用任何 DOM 元素。
考慮下面的新舊虛擬節點示例:
// 舊 vnode
const oldVNode = {type: 'div',children: [{ type: 'p', children: '1' },{ type: 'p', children: '2' },{ type: 'p', children: '3' }]
}// 新 vnode
const newVNode = {type: 'div',children: [{ type: 'p', children: '4' },{ type: 'p', children: '5' },{ type: 'p', children: '6' }]
}
上述代碼,我們會執行六次操作,三次卸載舊節點,三次添加新節點,但實際上,舊新節點都是 'p' 標簽,只是它們的文本內容變了。
理想情況下,我們只需要更新這些 'p' 標簽的文本內容就可以了,這樣只需要 3 次 DOM 操作,性能提升了一倍。
我們可以調整 patchChildren 函數,讓它只更新變化的部分:
function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代碼} else if (Array.isArray(n2.children)) {// 重新實現兩組子節點的更新方式// 新舊 childrenconst oldChildren = n1.childrenconst newChildren = n2.children// 遍歷舊的 childrenfor (let i = 0; i < oldChildren.length; i++) {// 調用 patch 函數逐個更新子節點patch(oldChildren[i], newChildren[i])}} else {// 省略部分代碼}
}
上述代碼,patch 函數在執行更新時,發現新舊子節點只有文本內容不同,因此只會更新其文本節點的內容
?
但是這段代碼假設新舊子節點的數量總是一樣的,實際上新舊節點的數量可能發生變化,如果新節點較多,我們應該添加節點,反之則刪除節點。
所以,我們應遍歷長度較短的那組子節點,以便盡可能多地調用 patch 函數進行更新。然后,比較新舊子節點組的長度,如果新組長度更長,就掛載新子節點;反之,就卸載舊子節點
function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代碼} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.children// 舊的一組子節點的長度const oldLen = oldChildren.length// 新的一組子節點的長度const newLen = newChildren.length// 兩組子節點的公共長度,即兩者中較短的那一組子節點的長度const commonLength = Math.min(oldLen, newLen)// 遍歷 commonLength 次for (let i = 0; i < commonLength; i++) {patch(oldChildren[i], newChildren[i], container)}// 如果 newLen > oldLen,說明有新子節點需要掛載if (newLen > oldLen) {for (let i = commonLength; i < newLen; i++) {patch(null, newChildren[i], container)}} else if (oldLen > newLen) {// 如果 oldLen > newLen,說明有舊子節點需要卸載for (let i = commonLength; i < oldLen; i++) {unmount(oldChildren[i])}}} else {// 省略部分代碼}
}
這樣,無論新舊子節點組的數量如何,我們的渲染器都能正確地掛載或卸載它們。
9.2 DOM 復用與 key 的作用
上面我們通過減少操作次數提高了性能,但仍有優化空間。
以新舊兩組子節點為例,它們的內容如下:
// oldChildren
[{ type: 'p' },{ type: 'div' },{ type: 'span' }
]// newChildren
[{ type: 'span' },{ type: 'p' },{ type: 'div' }
]
若使用之前的算法更新子節點,需要執行6次 DOM 操作。觀察新舊子節點,發現它們只是順序不同。
因此,最優處理方式是通過 DOM 移動來完成更新,而非頻繁卸載和掛載。為實現這一目標,需確保新舊子節點中存在可復用節點。
為判斷新子節點是否在舊子節點中出現,可以引入 key 屬性作為虛擬節點的標識。只要兩個虛擬節點的 type 和 key 屬性相同,我們認為它們相同,可以復用DOM。例如:
// oldChildren
[{ type: 'p', children: '1', key: 1 },{ type: 'p', children: '2', key: 2 },{ type: 'p', children: '3', key: 3 }
]// newChildren
[{ type: 'p', children: '3', key: 3 },{ type: 'p', children: '1', key: 1 },{ type: 'p', children: '2', key: 2 }
]
?我們根據子節點的 key 屬性,能夠明確知道新子節點在舊子節點中的位置,這樣就可以進行相應的 DOM 移動操作了。
注意 DOM 可復用并不意味著不需要更新,它可能內部子節點不一樣:
const oldVNode = { type: 'p', key: 1, children: 'text 1' }
const newVNode = { type: 'p', key: 1, children: 'text 2' }
所以補丁操作是在移動 DOM 元素之前必須完成的步驟,如下面的 patchChildren 函數所示:
function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代碼} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.children// 遍歷新的 childrenfor (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i]// 遍歷舊的 childrenfor (let j = 0; j < oldChildren.length; j++) {const oldVNode = oldChildren[j]// 如果找到了具有相同 key 值的兩個節點,說明可以復用,但仍然需要調用 patch 函數更新if (newVNode.key === oldVNode.key) {patch(oldVNode, newVNode, container)break // 這里需要 break}}}} else {// 省略部分代碼}
}
在這段代碼中,我們更新了新舊兩組子節點。通過兩層 for 循環,外層遍歷新的子節點,內層遍歷舊的子節點,我們尋找并更新了所有可復用的節點。
例如,有如下的新舊兩組子節點:
const oldVNode = {type: 'div',children: [{ type: 'p', children: '1', key: 1 },{ type: 'p', children: '2', key: 2 },{ type: 'p', children: 'hello', key: 3 }]
}const newVNode = {type: 'div',children: [{ type: 'p', children: 'world', key: 3 },{ type: 'p', children: '1', key: 1 },{ type: 'p', children: '2', key: 2 }]
}// 首次掛載
renderer.render(oldVNode, document.querySelector('#app'))
setTimeout(() => {// 1 秒鐘后更新renderer.render(newVNode, document.querySelector('#app'))
}, 1000);
運行上述代碼,1 秒鐘后,key 為 3 的子節點對應的真實 DOM 的文本內容將從 'hello' 更新為 'world'。讓我們仔細分析一下這段代碼在執行更新操作時的過程:
- 第一步,我們選取新的子節點組中的第一個子節點,即 key 為 3 的節點。然后在舊的子節點組中尋找具有相同 key 的節點。我們發現,舊子節點 oldVNode[2] 的 key 為 3,因此調用 patch 函數進行補丁操作。這個操作完成后,渲染器會將 key 為 3 的虛擬節點對應的真實 DOM 的文本內容從 'hello' 更新為 'world'。
- 第二步,我們取新的子節點組中的第二個子節點,即 key 為 1 的節點,并在舊的子節點組中尋找具有相同 key 的節點。我們發現,舊的子節點 oldVNode[0] 的 key 為 1,于是再次調用 patch 函數進行補丁操作。由于 key 為 1 的新舊子節點沒有任何差異,所以這里并未進行任何操作。
- 第三步,最后,我們取新的子節點組中的最后一個子節點,即 key 為 2 的節點,這一步的結果與第二步相同。
經過以上更新操作后,所有節點對應的真實 DOM 元素都已更新。
但真實 DOM 仍保持舊的子節點順序,即 key 為 3 的節點對應的真實 DOM 仍然是最后一個子節點。
然而在新的子節點組中,key 為 3 的節點已經變為第一個子節點,因此我們還需要通過移動節點來完成真實 DOM 順序的更新。
9.3 找到需要移動的元素
現在,我們已經能夠通過 key 值找到可復用的節點了。
下一步,我們確定哪些節點需要移動以及如何移動。我們逆向思考下,什么條件下節點無需移動。答案直觀:新舊子節點順序未變時,無需額外操作:
新舊子節點順序未變,舉例說明舊子節點索引:
- key 為 1 的節點在舊 children 數組中的索引為 0;
- key 為 2 的節點在舊 children 數組中的索引為 1;
- key 為 3 的節點在舊 children 數組中的索引為 2。
應用我們上節的更新算法:
- 第一步:取新的一組子節點中的第一個節點 p-1,它的 key 為 1。嘗試在舊的一組子節點中找到具有相同 key 值的可復用節點,發現能夠找到,并且該節點在舊的一組子節點中的索引為 0。
- 第二步:取新的一組子節點中的第二個節點 p-2,它的 key 為 2。嘗試在舊的一組子節點中找到具有相同 key 值的可復用節點,發現能夠找到,并且該節點在舊的一組子節點中的索引為 1。
- 第三步:取新的一組子節點中的第三個節點 p-3,它的 key 為 3。嘗試在舊的一組子節點中找到具有相同 key 值的可復用節點,發現能夠找到,并且該節點在舊的一組子節點中的索引為 2。
如果每次找到可復用節點,記錄他們原先在舊子節點的位置索引,把這些位置索引值按照先后順序排列,則可以得到一個序列:0、1、2。這是一個遞增的序列,在這種情況下不需要移動任何節點。
我們再來看看另外一個例子:
- 第一步:取新的一組子節點中的第一個節點 p-3,它的 key 為 3。嘗試在舊的一組子節點中找到具有相同 key 值的可復用節點,發現能夠找到,并且該節點在舊的一組子節點中的索引為 2。
- 第二步:取新的一組子節點中的第二個節點 p-1,它的 key 為 1。嘗試在舊的一組子節點中找到具有相同 key 值的可復用節點,發現能夠找到,并且該節點在舊的一組子節點中的索引為 0。
- 到了這一步我們發現,索引值遞增的順序被打破了。節點 p-1 在舊 children 中的索引是 0,它小于節點 p-3 在舊 children 中的索引 2。這說明節點 p-1 在舊 children 中排在節點 p-3 前面,但在新的 children 中,它排在節點 p-3 后面。因此,我們能夠得出一個結論:節點 p-1 對應的真實 DOM 需要移動。
- 第三步:取新的一組子節點中的第三個節點 p-2,它的 key 為 2。嘗試在舊的一組子節點中找到具有相同 key 值的可復用節點,發現能夠找到,并且該節點在舊的一組子節點中的索引為 1。
- 到了這一步我們發現,節點 p-2 在舊 children 中的索引 1 要小于節點 p-3 在舊 children 中的索引 2。這說明,節點 p-2 在舊 children 中排在節點 p-3 前面,但在新的 children 中,它排在節點 p-3 后面。因此,節點 p-2 對應的真實 DOM 也需要移動。
在上面的例子中,我們得出了節點 p-1 和節點 p-2 需要移動的結論。這是因為它們在舊 children 中的索引要小于節點 p-3 在舊 children 中的索引。如果我們按照先后順序記錄在尋找節點過程中所遇到的位置索引,將會得到序列:2、0、1。可以發現,這個序列不具有遞增的趨勢。
我們可以用 lastIndex 變量存儲整個尋找過程中遇到的最大索引值,如下面的代碼所示:
function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代碼} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.children// 用來存儲尋找過程中遇到的最大索引值let lastIndex = 0for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i]for (let j = 0; j < oldChildren.length; j++) {const oldVNode = oldChildren[j]if (newVNode.key === oldVNode.key) {patch(oldVNode, newVNode, container)if (j < lastIndex) {// 如果當前找到的節點在舊 children 中的索引小于最大索引值 lastIndex,// 說明該節點對應的真實 DOM 需要移動} else {// 如果當前找到的節點在舊 children 中的索引不小于最大索引值,// 則更新 lastIndex 的值lastIndex = j}break // 這里需要 break}}}} else {// 省略部分代碼}
}
上述代碼,如果新舊節點的 key 值相同,我們就找到了可以復用的節點。我們比較這個節點在舊子節點數組中的索引 j 與 lastIndex。
如果 j 小于 lastIndex,說明當前 oldVNode 對應的真實 DOM 需要移動。
否則,不需要移動。并將變量 j 的值賦給 lastIndex,以確保尋找節點過程中,變量 lastIndex 始終存儲著當前遇到的最大索引值。
9.4 如何移動元素
移動節點指的是,移動一個虛擬節點所對應的真實 DOM 節點,并不是移動虛擬節點本身。
既然移動的是真實 DOM 節點,那么就需要取得對它的引用才行。當一個虛擬節點被掛載后,其對應的真實 DOM 節點會存儲在它的 vnode.el 屬性中:
因此,我們可以通過 vnode.el 屬性取得它對應的真實 DOM 節點。
當更新操作發生時,渲染器會調用 patchElement 函數在新舊虛擬節點之間進行打補丁:
function patchElement(n1, n2) {// 新的 vnode 也引用了真實 DOM 元素const el = n2.el = n1.el// 省略部分代碼
}
上述代碼 patchElement 函數首先將舊節點的 n1.el 屬性賦值給新節點的 n2.el 屬性,這個賦值的意義其實就是 DOM 元素的復用。
在復用了 DOM 元素之后,新節點也將持有對真實 DOM 的引用:
此時無論是新舊節點,都引用著真實 DOM,在此基礎上,我們就可以進行 DOM 移動了。
為了闡述如何移動 DOM,,我們仍然引用上一節的更新案例:
它的更新步驟如下。
- 第一步:在新的子節點集合中選取第一個節點 p-3(key 為 3),并在舊的子節點集合中尋找具有相同 key 的可復用節點。找到了這樣的節點,其在舊集合中的索引為 2。由于當前 lastIndex 為 0,且 2 大于 0,因此,p-3 的實際 DOM 無需移動,但需要將 lastIndex 更新為 2。
- 第二步:選取新集合中的第二個節點 p-1(key 為 1),并嘗試在舊集合中找到相同 key 的可復用節點。找到了這樣的節點,其在舊集合中的索引為 0。此時,由于 lastIndex 為 2,且 0 小于 2,所以,p-1 的實際 DOM 需要移動。此時我們知道,**新 children 的順序即為更新后實際 DOM 應有的順序。**因此,p-1 在新 children 中的位置決定了其在更新后實際 DOM 中的位置。由于 p-1 在新 children 中排在 p-3 后面,因此我們需要將 p-1 的實際 DOM 移動到 p-3 的實際 DOM 后面。移動后的實際 DOM 順序為 p-2、p-3、p-1:
-
把節點 p-1 對應的真實 DOM 移動到節點 p-3 對應的真實 DOM 后面 - 第三步:選取新集合中的第三個節點 p-2(key 為 2),并嘗試在舊集合中找到相同 key 的可復用節點。找到了這樣的節點,其在舊集合中的索引為1。此時,由于 lastIndex 為 2,且 1 小于 2,所以,p-2 的實際 DOM 需要移動。此步驟與步驟二類似,我們需要將 p-2 的實際 DOM 移動到 p-1 的實際 DOM 后面。經過移動后,實際 DOM 的順序與新的子節點集合的順序相同,即為:p-3、p-1、p-2。至此,更新操作完成:
-
把節點 p-2 對應的真實 DOM 移動到節點 p-1 對應的真實 DOM 后面
接下來,我們來看一下如何實現這個過程。具體的代碼如下:
function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代碼} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.childrenlet lastIndex = 0for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i]let j = 0for (j; j < oldChildren.length; j++) {const oldVNode = oldChildren[j]if (newVNode.key === oldVNode.key) {patch(oldVNode, newVNode, container)if (j < lastIndex) {// 代碼運行到這里,說明 newVNode 對應的真實 DOM 需要移動// 先獲取 newVNode 的前一個 vnode,即 prevVNodeconst prevVNode = newChildren[i - 1]// 如果 prevVNode 不存在,則說明當前 newVNode 是第一個節點,它不需要移動if (prevVNode) {// 由于我們要將 newVNode 對應的真實 DOM 移動到 prevVNode 所對應真實 DOM 后面,// 所以我們需要獲取 prevVNode 所對應真實 DOM 的下一個兄弟節點,并將其作為錨點const anchor = prevVNode.el.nextSibling// 調用 insert 方法將 newVNode 對應的真實 DOM 插入到錨點元素前面,// 也就是 prevVNode 對應真實 DOM 的后面insert(newVNode.el, container, anchor)}} else {lastIndex = j}break}}}} else {// 省略部分代碼}
}
上述代碼中,如果 j < lastIndex 成立,則說明當前 newVNode 對應的真實 DOM 需要移動。
根據之前的分析可知,我們需要獲取當前 newVNode 節點的前一個虛擬節點 newChildren[i - 1],然后使用 insert 函數完成節點的移動,其中 insert 函數依賴瀏覽器原生的 insertBefore 函數。如下所示:
const renderer = createRenderer({// 省略部分代碼insert(el, parent, anchor = null) {// insertBefore 需要錨點元素 anchorparent.insertBefore(el, anchor)}// 省略部分代碼
})
9.5 添加新元素
上圖,我們有一個新的節點p-4, 它的 key 值為 4,這個節點在舊的節點集中不存在。該新增節點我們應該掛載:
- 找到新節點
- 將新節點掛載到正確位置
根據上圖,我們開始模擬執行簡單 Diff 算法的更新邏輯:
- 第一步:我們首先檢查新的節點集中的第一個節點 p-3。這個節點在舊的節點集中存在,因此我們不需要移動對應的 DOM 元素,但是我們需要將變量lastIndex的值更新為 2。
- 第二步:取新的一組子節點中第二個節點 p-1,它的 key 值為 1,嘗試在舊的一組子節點中尋找可復用的節點。發現能夠找到,并且該節點在舊的一組子節點中的索引值為 0。此時變量 lastIndex 的值為 2,索引值 0 小于 lastIndex 的值 2,所以節點 p-1 對應的真實 DOM 需要移動,并且應該移動到節點 p-3 對應的真實DOM 后面。移動后,DOM 的順序將變為 p-2、p-3、p-1:
-
- 第三步:我們現在查看新的節點集中的第三個節點 p-4。在舊的節點集中,我們找不到這個節點,我們需要觀察節點 p-4 在新的一組子節點中的位置。由于節點 p-4 出現在節點 p-1 后面,所以我們應該把節點 p-4 掛載到節點 p-1 所對應的真實 DOM 后面。DOM 元素后面。掛載后,DOM的順序將變為 p-2、p-3、p-1、p-4:
-
- 第四步:最后,我們查看新的節點集中的第四個節點 p-2。在舊的節點集中,這個節點的索引值為 1,這個值小于 lastIndex 的值 2,因此我們需要移動 p-2 對應的 DOM 元素。應該移動到節點 p-4 對應的真實DOM 后面。
-
在此,我們看到真實 DOM 的順序為:p-3、p-1、p-4、p-2。這表明真實 DOM 的順序已經與新子節點的順序一致,更新已經完成。
接下來,讓我們通過 patchChildren 函數的代碼實現來詳細講解:
function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代碼} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.childrenlet lastIndex = 0for (let i = 0; i < newChildren.length; i++) {const newVNode = newChildren[i]let j = 0// 在第一層循環中定義變量 find,代表是否在舊的一組子節點中找到可復用的節點,// 初始值為 false,代表沒找到let find = falsefor (j; j < oldChildren.length; j++) {const oldVNode = oldChildren[j]if (newVNode.key === oldVNode.key) {// 一旦找到可復用的節點,則將變量 find 的值設為 truefind = truepatch(oldVNode, newVNode, container)if (j < lastIndex) {const prevVNode = newChildren[i - 1]if (prevVNode) {const anchor = prevVNode.el.nextSiblinginsert(newVNode.el, container, anchor)}} else {lastIndex = j}break}}// 如果代碼運行到這里,find 仍然為 false,// 說明當前 newVNode 沒有在舊的一組子節點中找到可復用的節點// 也就是說,當前 newVNode 是新增節點,需要掛載if (!find) {// 為了將節點掛載到正確位置,我們需要先獲取錨點元素// 首先獲取當前 newVNode 的前一個 vnode 節點const prevVNode = newChildren[i - 1]let anchor = nullif (prevVNode) {// 如果有前一個 vnode 節點,則使用它的下一個兄弟節點作為錨點元素anchor = prevVNode.el.nextSibling} else {// 如果沒有前一個 vnode 節點,說明即將掛載的新節點是第一個子節點// 這時我們使用容器元素的 firstChild 作為錨點anchor = container.firstChild}// 掛載 newVNodepatch(null, newVNode, container, anchor)}}} else {// 省略部分代碼}
}
上述代碼,我們通過外層循環中定義的變量 find,查找是否存在可復用的節點。
如果內層循環結束后,find 的值仍為 false,說明當前 newVNode 是全新的節點,需要進行掛載。
掛載的位置由 anchor 確定,這個 anchor 可以是 newVNode 的前一個虛擬節點的下一個兄弟節點,或者容器元素的第一個子節點。
現在,我們需要調整 patch 函數以支持接收第四個參數 anchor,如下所示:
// patch 函數需要接收第四個參數,即錨點元素
function patch(n1, n2, container, anchor) {// 省略部分代碼if (typeof type === 'string') {if (!n1) {// 掛載時將錨點元素作為第三個參數傳遞給 mountElement 函數mountElement(n2, container, anchor)} else {patchElement(n1, n2)}} else if (type === Text) {// 省略部分代碼} else if (type === Fragment) {// 省略部分代碼}
}// mountElement 函數需要增加第三個參數,即錨點元素
function mountElement(vnode, container, anchor) {// 省略部分代碼// 在插入節點時,將錨點元素透傳給 insert 函數insert(el, container, anchor)
}
9.6 移除不存在的元素
在更新子節點時,不僅會遇到新增元素,還會出現元素被刪除的情況:
?
假設在新的子節點組中,節點 p-2 已經不存在,這說明該節點被刪除了。
我們像上面一樣模擬執行更新邏輯,這之前我們先看看新舊兩組子節點以及真實 DOM 節點的當前狀態:
- 第一步:取新的子節點組中的第一個節點 p-3,它的 key 值為 3。在舊的子節點組中尋找可復用的節點,發現索引為 2 的節點可復用,此時變量 lastIndex 的值為 0,索引 2 不小于 lastIndex 的值 0,所以 節點 p-3 對應的真實 DOM 不需要移動,但需要更新變量 lastIndex 的值為 2。
- 第二步,取新的子節點組中的第二個節點 p-1,它的 key 值為 1。在舊的子節點組中發現索引為 0 的節點可復用。 并且該節點在舊的一組子節點中的索引值為 0。此時變量 lastIndex 的值為 2,索引 0 小于 lastIndex 的值 2,所以節 點 p-1 對應的真實 DOM 需要移動,并且應該移動到節點 p-3 對 應的真實 DOM 后面:
-
- 最后,我們發現節點 p-2 對應的真實 DOM 仍然存在,所以需要增加邏輯來刪除遺留節點。
我們可以在基本更新結束后,遍歷舊的子節點組,然后去新的子節點組中尋找具有相同 key 值的節點。如果找不到,說明應刪除該節點,如下面 patchChildren 函數的代碼所示:
function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 省略部分代碼} else if (Array.isArray(n2.children)) {const oldChildren = n1.childrenconst newChildren = n2.childrenlet lastIndex = 0for (let i = 0; i < newChildren.length; i++) {// 省略部分代碼}// 上一步的更新操作完成后// 遍歷舊的一組子節點for (let i = 0; i < oldChildren.length; i++) {const oldVNode = oldChildren[i]// 拿舊子節點 oldVNode 去新的一組子節點中尋找具有相同 key 值的節點const has = newChildren.find(vnode => vnode.key === oldVNode.key)if (!has) {// 如果沒有找到具有相同 key 值的節點,則說明需要刪除該節點// 調用 unmount 函數將其卸載unmount(oldVNode)}}} else {// 省略部分代碼}
}
上述代碼,在上一步的更新操作完成之后,我們還需要遍歷舊的一組子節點,目的是檢查舊子節點在新的一組子節點中是否仍然存在,如果已經不存在了,則調用 unmount 函數將其卸載。
9.7 總結
本章我們討論 Diff 算法的作用,Diff 是用來計算兩組子節點的差異,并最大程度復用 DOM 元素。
最開始我們采用了一種簡單的方式來更新子節點,即卸載所有舊子節點,再掛載所有新子節點。然而這種操作無疑是非常消耗性能的。
于是我們改進為:遍歷新舊兩組子節點中數量較少的那一組,并逐個調用 patch 函數進行打補丁,然后比較新舊兩組子節點的數量,如果新的一組子節點數量更多,說明有新子節點需要掛載;否則說明在舊的一組子節點中,有節點需要卸載。
然后我們討論了 key 值作用,,它就像虛擬節點 的“身份證號”。渲染器通過 key 找到可復用元素,避免對 DOM 元素過多的銷毀重建。
接著我們討論了簡單 Diff 邏輯:在新的一組節點中去尋找舊節點可復用的元素。如果找到了,則記錄該節點的位置索引。我們把這個位置索引稱為最大索引。在整個更新過程中,如果一個節點的索引值小于最大索引,則說明該節點對應的真實 DOM 元素需要移動。
最后,我們講解了渲染器是如何移動、添加、刪除 虛擬節點所對應的 DOM 元素的。