Vue3 beta
版本已經發布,新版本對 Virtual DOM diff
算法作了改進,性能提高了 1.3 - 2
倍。本文跟你們一塊兒學習一下新版 Virtual DOM diff
算法。node
本文將會講述如下內容算法
Vue3
的 VDOM diff
算法Vue3
是如何利用最長遞增子序列優化 diff
算法的Vue2
中的 VDOM diff
算法,分析 Vue2
diff
算法的不足,以及 Vue3
中是如何優化從而提高性能的假設有如下新、舊兩組數據,咱們如何找出哪些數據是新增、哪些如要刪除和移動?數組
傳統的作法bash
這種作法實現沒有問題,可是效率卻很低。大佬固然不屑於這種 low
的作法,咱們先看 Vue3
中是如何作的,最後再回顧一下 Vue2
中的作法markdown
// c1 舊數據 ["a","b","c","d","g","f"] // c2 新數據 ["a","e","b","d","c","f"] 複製代碼
Web
中對數組的操做大體有新增、刪除、排序。因此算法針對這幾種操做作了優化。less
原理大體以下:oop
["a"]
進行 patch
,遇到不相同的節點中止比較["f"]
進行 patch
,遇到不相同的節點中止比較c1
中的全部節點都已經比較完了,c2
中剩餘沒有比較的節點都是新數據,執行 mount
c2
中的全部節點都已經比較完了,c1
中剩餘沒有比較的節點都是須要刪除的,執行 unmount
c1
c2
中都有剩餘節點,對剩餘節點進行比較unmount
diff
算法的核心,也是比較難理解的部分前四步針對新增、刪除,第五步針對排序。性能
通過前面四步後,會獲得以下數據。第五步是最剩餘的數據進行比較。學習
// c1 剩餘 ["b","c","d","g"] // c2 剩餘 ["e","b","d","c"]複製代碼
從第一個節點開始比較優化
patch
i
自增一次const patchKeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, parentAnchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { let i = 0 //從左往右開始位置 const l2 = c2.length let e1 = c1.length - 1 // 舊節點結束位置 let e2 = l2 - 1 // 新節點結束位置 // 1. sync from start // (a b) c // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized ) } else { break } i++ } }複製代碼
從最後一個節點開始比較
patch
e1, e2
往前移動一次// a (b c) // d e (b c) while (i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized ) } else { break } e1-- e2-- }複製代碼
若是 i > e1
說明舊數據已經比較完了,那麼新數據中剩餘沒有比較的節點(i
到 e2
之間的節點)都是新增的
// (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 if (i > e1) { if (i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { patch( null,// 舊元素爲 null,此時爲新增 (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG ) i++ } } }複製代碼
若是 i > e2
說明新數據已經全都比較完了,舊數據中沒有比較的節點( i
和 e1
之間的節點)都是須要刪除的。
// (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ } }複製代碼
上面四個步驟已經能知足頁面上常見的新增和刪除操做,若是是排序或者數據被從新賦值,上面四個步驟是不知足的。
// c1 舊數據 ["a","b","c","d","e","f","g"] // c2 新數據 ["a","c","d","b","f","i","g"] 複製代碼
通過上述四個步驟處理以後,剩餘的節點以下
// c1 剩餘 ["b","c","d","e","f"] // c2 剩餘 ["c","d","b","f","i"]複製代碼
能夠看出
b
、c
、d
、f
都被移動了e
被刪除了i
是新增的想要知道 c1
中剩餘的節點是被刪除仍是移動了。確定要遍歷 c1
中的剩餘數據去 c2
中查找。
key
說明該節點被移動了,要記錄下新的 index
方便後續移動遍歷 c2 中剩餘的數據,存儲 key
=> index
關係,方便後續 c1
遍歷的時候進行查找
const s1 = i // 舊數據開始位置 const s2 = i // 新數據開始位置 // 5.1 build key:index map for newChildren const keyToNewIndexMap: Map<string | number, number> = new Map() for (i = s2; i <= e2; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (nextChild.key != null) { if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) { warn( `Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.` ) } keyToNewIndexMap.set(nextChild.key, i) } }複製代碼
此時的 keyToNewIndexMap
以下
{"c" => 1} {"d" => 2} {"b" => 3} {"f" => 4} {"i" => 5}複製代碼
newIndexToOldIndexMap
數組,用於記錄新元素位置=>舊元素位置的對應關係。0
keyToNewIndexMap
中查找對應的 newIndex
unmount
oldIndex
+1
)關係, patch
newIndex
不是趨勢遞增的,說明有節點須要移動newIndexToOldIndexMap
中 oldIndex
爲 0
的就是新增的節點思考:爲何要 oldIndex +1
?
假如新、舊數據以下
// c1 舊數據 ["c","a","b","d","e","f"] // c2 新數據 ["a","c","d","b","f","i"]複製代碼
c2
中的 c
在 c1
中對應的 oldIndex
就是 0
,由於 0
在 newIndexToOldIndexMap
是特殊值,表明新增的節點。因此不能將 0
存入 newIndexToOldIndexMap
,所以 oldIndex + 1
了。這裏是爲了計算最長遞增子序列。
// c1 舊數據
["a","b","c","d","e","f","g"]
// c2 新數據
["a","c","d","b","f","i","g"]
c1
中剩餘數據遍歷結束,newIndexToOldIndexMap
以下
//newIndexToOldIndexMap [3,4,2,6,0] //對應 ["c","d","b","f","i"]複製代碼
能夠看出 c
, d
, b
, f
須要移動,i
須要新增。
這時只須要遍歷 newIndexToOldIndexMap
• 0
表明是新增的數據,執行 mount
• 非 0
的數據,從 c1
中找到並移動到對應的 newIndex
前面便可
以下:
c
移動到 b
前d
移動到 b
前i
增長let j let patched = 0 const toBePatched = e2 - s2 + 1// 剩餘新節點長度 let moved = false //是否須要移動 // used to track whether any node has moved let maxNewIndexSoFar = 0 // works as Map<newIndex, oldIndex> // Note that oldIndex is offset by +1 // and oldIndex = 0 is a special value indicating the new node has // no corresponding old node. // used for determining longest stable subsequence const newIndexToOldIndexMap = new Array(toBePatched) for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 for (i = s1; i <= e1; i++) { const prevChild = c1[i] // 剩餘新節點已經處理完,剩餘的舊節點都須要 unmount if (patched >= toBePatched) { // all new children have been patched so this can only be a removal unmount(prevChild, parentComponent, parentSuspense, true) continue } let newIndex if (prevChild.key != null) { newIndex = keyToNewIndexMap.get(prevChild.key) } else { // key-less node, try to locate a key-less node of the same type for (j = s2; j <= e2; j++) { if ( newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode) ) { newIndex = j break } } } if (newIndex === undefined) { unmount(prevChild, parentComponent, parentSuspense, true) } else { newIndexToOldIndexMap[newIndex - s2] = i + 1 // oldIndex + 1 // maxNewIndexSoFar 是否是遞增的趨勢,說明有節點須要移動 if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { moved = true } patch( prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, optimized ) patched++ } }複製代碼
// c1 舊數據
["a","b","c","d","e","f","g"]
// c2 新數據
["a","c","d","b","f","i","g"]
要將 ["b", "c" ,"d"]
變成 ["c", "d", "b"]
,c
, d
不用動,只須要將 b
移動到 d
以後就能夠了,不須要將 c
和 d
分別移動到 b
以前。
如何找到不須要移動的元素,減小移動次數?
在計算機科學中,最長遞增子序列(longest increasing subsequence)問題是指,在一個給定的數值序列中,找到一個子序列,使得這個子序列元素的數值依次遞增,而且這個子序列的長度儘量地大。最長遞增子序列中的元素在原序列中不必定是連續的。
對於如下的原始序列
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
最長遞增子序列爲
0, 2, 6, 9, 11, 15
值得注意的是原始序列的最長遞增子序列並不必定惟一
對於該原始序列,實際上還有如下兩個最長遞增子序列
0, 4, 6, 9, 11, 15
0, 4, 6, 9, 13, 15
0, 2, 6, 9, 13, 15複製代碼
newIndexToOldIndexMap
[3, 4, 2, 6, 0]
中遞增的子序列爲 3, 4, 6
。對應的索引爲[0, 1, 3]
,分別對應對應 c
, d
, f
。也就是說 c
, d
, f
是不須要移動的。
遍歷 c2
中剩餘的節點
newIndexToOldIndexMap
中對應的 oldIndex
是 0
新增const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR j = increasingNewIndexSequence.length - 1 // looping backwards so that we can use last patched node as anchor for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i const nextChild = c2[nextIndex] as VNode const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor // 新增 if (newIndexToOldIndexMap[i] === 0) { // mount new patch( null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG ) } else if (moved) {// 移動 // move if: // There is no stable subsequence (e.g. a reverse) // OR current node is not among the stable sequence if (j < 0 || i !== increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) } else { j-- } } } // https://en.wikipedia.org/wiki/Longest_increasing_subsequence function getSequence(arr: number[]): number[] { const p = arr.slice() const result = [0] let i, j, u, v, c const len = arr.length for (i = 0; i < len; i++) { const arrI = arr[i] if (arrI !== 0) { j = result[result.length - 1] if (arr[j] < arrI) { p[i] = j result.push(i) continue } u = 0 v = result.length - 1 while (u < v) { c = ((u + v) / 2) | 0 if (arr[result[c]] < arrI) { u = c + 1 } else { v = c } } if (arrI < arr[result[u]]) { if (u > 0) { p[i] = result[u - 1] } result[u] = i } } } u = result.length v = result[u - 1] while (u-- > 0) { result[u] = v v = p[v] } return result }複製代碼
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)) { // Vnode moved right 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)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element 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) } }複製代碼
// c1 舊數據 ["a","b","c","d","g","f"] // c2 新數據 ["a","e","b","d","c","f"]複製代碼
Vue2
中的 diff
是個雙指針循環
patchVnode
patchVnode
index
找不到就新增,找到就移動新數據先比較完,剩餘的未比較的舊數據都須要刪除
舊數據先比較完,剩餘的未比較的新數據都須要新增
前面四步比較結束以後,剩餘未比較的新數據以下
["e","b","d","c"]複製代碼
跟 Vue3
中前四步結束後獲得的結果是同樣的。
Vue2
中遍歷剩餘的新數據去舊數據中查找是在循環的最後,也就是說每一次遍歷上面的 if
都會執行。
Vue3
中利用最長遞增子序列優化了這一點,直接找到須要移動的節點進行移動操做。
由於首尾的比較是爲了對應節點移動的狀況,經過最長遞增子序列直接找到須要移動的節點,也就再也不須要首、尾的對比了。
理解了 Vue3
的 diff
算法
diff
算法對比了 Vue2
的 diff
算法