前言 前端的虚拟 DOM 框架,一般是用 js 树结构来对视图层进行抽象表达,通过对树的遍历创建 UI 元素,渲染到视图。 如果想要对视图进行更新,这时候会产生一颗新的树,显然根据新的数据全量重新创建 UI 元素,渲染到视图是极其消耗性能的。 因此需要对新老两颗树进行对比,找到差异点,将差异部分更新到存在的视图。
为了保证性能,我们需要考虑以下下几点:
尽可能复用现有 UI 元素
减少对 UI 元素的操作
减少遍历次数
在实际场景中,其实基本不会遇到子树的层级跃迁移动,往往只会对同一层级的元素进行更新。所以没必要进行跨层级的比较。于是就变成了两个数组之间的比较。
正文 基于以上可以抽象为一道算法题: 存在 oldChildren newChildren 两个数组,数组元素在该数组内都是唯一且不重复的,UIChildren 初始值为 oldChildren 的副本,
尽可能少地操作读取 UIChildren
尽可能少地读写 oldChildren 和 newChildren
优先满足第一点
尽可能利用 UIChildren 中的现有元素(newChildren,oldChildren 中的交集元素在 UIChildren 中 只能被移动不能被删除), 通过对比 oldChildren 和 newChildren 实现将 UIChildren 变为 newChildren 的副本 例如; oldChildren 为 [1, 3, 4] newChildren 为 [1, 2, 4] 那么 UIChildren 的初始值为 [1, 3, 4] 最后返回的 UIChildren 为 [1, 2, 4] 值得注意的是,对 UIChildren 进行操作的过程中,1 和 4 是属于可以复用的元素,所以只可以被移动但是不可以被删除的元素。
1 2 3 4 5 function diff (oldChildren, newChildren ) { let UIChildren = oldChildren.map(v => v) return UIChildren }
对 UIChildren 进行操作提供了以下几个 API(其实是为了模拟 DOM 的 API,它和数组用法上设计风格差异很大,一个基于 index,一个基于元素)
inesertBefore(UIChildren, child, refChild) 讲一个元素插入到某一个元素前面
removeChild(UIChildren, child) 移除某一个元素
nextSibling(UIChildren, child) 获取某一个元素之后的元素 具体实现参见
React 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 export function diff (oldChildren, newChildren ) { let UIChildren = oldChildren.map(v => v); let lastIndex = 0 ; if (newChildren.length === 0 ) { UIChildren = [] } for (let n = 0 ; n < newChildren.length; n++) { const newNode = newChildren[n] let hasFind = false ; let o = 0 ; for (o; o < oldChildren.length; o++) { const oldNode = oldChildren[o] if (newNode === oldNode) { hasFind = true if (o < lastIndex) { const refNode = nextSibling(UIChildren, newChildren[n - 1 ]) inesertBefore(UIChildren, oldNode, refNode) } else { lastIndex = o } break } } if (!hasFind) { const refNode = n - 1 < 0 ? oldChildren[0 ] : nextSibling(UIChildren, newChildren[n - 1 ]) inesertBefore(UIChildren, newChildren[n], refNode) } for (let i = 0 ; i < oldChildren.length; i++) { const item = oldChildren[i]; const hasFind = newChildren.find(v => v === item) if (!hasFind) { removeChild(UIChildren, item) } } } return UIChildren }
代码参见
Snabddom / Vue2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 export function diff (oldChildren, newChildren ) { let UIChildren = oldChildren.map(v => v) let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldChildren.length - 1 let newEndIdx = newChildren.length - 1 let oldStartNode = oldChildren[0 ] let newStartNode = newChildren[0 ] let oldEndNode = oldChildren[oldEndIdx] let newEndNode = newChildren[newEndIdx] while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (!oldStartNode) { oldStartNode = oldChildren[++oldStartIdx] } else if (!oldEndNode) { oldEndNode = oldChildren[--oldEndIdx] } else if (oldStartNode === newStartNode) { oldStartNode = oldChildren[++oldStartIdx] newStartNode = newChildren[++newStartIdx] } else if (oldEndNode === newEndNode) { oldEndNode = oldChildren[--oldEndIdx] newEndNode = newChildren[--newEndIdx] } else if (oldStartNode === newEndNode) { inesertBefore(UIChildren, oldStartNode, nextSibling(UIChildren, oldEndNode)) oldStartNode = oldChildren[++oldStartIdx] newEndNode = newChildren[--newEndIdx] } else if (oldEndNode === newStartNode) { inesertBefore(UIChildren, oldEndNode, oldStartNode) oldEndNode = oldChildren[--oldEndIdx] newStartNode = newChildren[++newStartIdx] } else { const newEle = newChildren[newStartIdx] const oldIdx = oldChildren.indexOf(newEle) if (oldIdx === -1 ) { inesertBefore(UIChildren, newEle, oldStartNode) } else { inesertBefore(UIChildren, oldChildren[oldIdx], oldStartNode) oldChildren[oldIdx] = undefined } newStartNode = newChildren[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { const before = newChildren[newEndIdx + 1 ] == null ? null : newChildren[newEndIdx + 1 ] for (let i = newStartIdx; i <= newEndIdx; i++) { inesertBefore(UIChildren, newChildren[i], before) } } else if (newEndIdx < newStartIdx) { for (let i = oldStartIdx; i <= oldEndIdx; i++) { removeChild(UIChildren, oldChildren[i]) } } return UIChildren }
代码参见
Inferno / Vue3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 export function diff (oldChildren, newChildren ) { const UIChildren = oldChildren.map((v ) => v); let oldEndIndex = oldChildren.length - 1 ; let newEndIndex = newChildren.length - 1 ; let i = 0 ; let oldEndNode = oldChildren[i]; let newEndNode = newChildren[i]; outer: { while (oldEndNode === newEndNode) { i++; if (i > oldEndIndex || i > newEndIndex) { break outer; } oldEndNode = oldChildren[i]; newEndNode = newChildren[i]; } oldEndNode = oldChildren[oldEndIndex]; newEndNode = newChildren[newEndIndex]; while (oldEndNode === newEndNode) { newEndIndex--; oldEndIndex--; if (i > oldEndIndex || i > newEndIndex) { break outer; } oldEndNode = oldChildren[oldEndIndex]; newEndNode = newChildren[newEndIndex]; } } if (i > oldEndIndex && i <= newEndIndex) { const refNode = newChildren[newEndIndex + 1 ] == null ? null : newChildren[newEndIndex + 1 ] while (i <= newEndIndex) { inesertBefore(UIChildren, newChildren[i++] ,refNode) } } else if (i > newEndIndex) { while (i <= oldEndIndex) { removeChild(UIChildren, oldChildren[i++]) } } else { const newRemaining = newEndIndex - i + 1 const source = new Array (newRemaining).fill(-1 ) const oldStartIdx = i const newStartIdx = i let shouldMove = false let lastIndex = 0 const keyIndexMap = {} for (let k = newStartIdx; k <= newEndIndex; k++) { keyIndexMap[newChildren[k]] = k } let patched = 0 for (let i = oldStartIdx; i <= oldEndIndex; i++) { const oldNode = oldChildren[i] if (patched < newRemaining) { const k = keyIndexMap[oldNode] if (typeof k !== 'undefined' ) { patched ++ source[k - newStartIdx] = i if (k < lastIndex) { shouldMove = true } else { lastIndex = k } } else { removeChild(UIChildren, oldNode) } } else { removeChild(UIChildren, oldNode) } } if (shouldMove) { const seq = lis(source) let j = seq.length - 1 for (let i = newRemaining - 1 ; i>= 0 ; i --) { if (source[i] === -1 ) { const curIndex = i + newStartIdx const newNode = newChildren[curIndex] const refNode = newChildren[curIndex + 1 ] == null ? null : newChildren[curIndex + 1 ] inesertBefore(UIChildren, newNode, refNode) } else if (i !== seq[j]) { const curIndex = i + newStartIdx const newNode = newChildren[curIndex] const refNode = newChildren[curIndex + 1 ] == null ? null : newChildren[curIndex + 1 ] inesertBefore(UIChildren, newNode, refNode) } else { j-- } } } } return UIChildren; }
代码参见
参考