先阅读尤大的文章:
https://www.zhihu.com/question/31809713/answer/53544875
比较innerHTML和Virtual DOM的重绘性能消耗
innerHTML:render html string O(template size)+重新创建所有DOM元素O(DOM size)
Vitual DOM :render Vitual DOM +diff O(template size)+必要的DOM更新O
Vitual DOM render+diff显然比渲染html字符慢,但是它依然his纯js层面计算,比DOM操作而言,便宜了很多
diff算法: 一、是什么 diff算法是一种同层的树节点进行比较的高效算法
两个特点:
比较只会在同层级进行,不会跨层级比较
在diff比较过程中,循环会从两边向中间比较
diff
算法的在很多场景下都有应用,在 vue
中,作用于虚拟 dom
渲染成真实 dom
的新旧 VNode
节点比较
二、比较方式 diff
整体策略为:深度优先,同层比较
比较只会在同层级进行, 不会跨层级比较
比较的过程中,循环从两边向中间收拢
下面举个vue
通过diff
算法更新的例子:
新旧VNode
节点如下图所示:
第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff
后的第一个真实节点,同时旧节点endIndex
移动到C,新节点的 startIndex
移动到了 C
第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff
后创建了 C 的真实节点插入到第一次创建的 B 节点后面。同时旧节点的 endIndex
移动到了 B,新节点的 startIndex
移动到了 E
第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex
移动到了 A。旧节点的 startIndex
和 endIndex
都保持不动
第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff
后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex
移动到了 B,新节点的 startIndex
移动到了 B
第五次循环中,情形同第四次循环一样,因此 diff
后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex
移动到了 C,新节点的 startIndex 移动到了 F
新节点的 startIndex
已经大于 endIndex
了,需要创建 newStartIdx
和 newEndIdx
之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面
三、原理分析 当数据发生改变时,set
方法会调用Dep.notify
通知所有订阅者Watcher
,订阅者就会调用patch
给真实的DOM
打补丁,更新相应的视图
Vue2中使用双端diff算法: 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 function patchKeyedChildren (n1,n2,container ) { const oldChildren = n1.children const newChildren = n2.children let oldStartIdx = 0 let oldEndIdx = oldChildren.length-1 let newStartIdx = 0 let newEndIdx = newChildren.length-1 let oldStartVnode = oldChildren[oldStartIdx] let oldEndVnode = oldChildren[oldEndIdx] let newStartVnode = newChildren[newStartIdx] let newEndVnode = newChildren[newEndIdx] while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (!oldStartVnode) { oldStartVnode = oldChildren[++oldStartIdx] }else if (!oldEndVnode){ oldEndVnode = oldChildren[--oldEndIdx] } else if (oldStartVnode.key === newStartVnode.key){ patch(oldStartVnode,newStartVnode,container) oldStartVnode = oldChildren[++oldStartIdx] newStartVnode = newChildren[++newStartIdx] } else if (oldEndVnode.key === newEndVnode.key){ patch(oldEndVnode,newEndVnode,container) oldEndVnode = oldChildren[--oldEndIdx] newEndVnode = newChildren[--newEndIdx] }else if (oldStartVnode.key === newEndVnode.key){ patch(oldStartVnode,newEndVnode,container) insert(oldStartVnode.el,container,oldEndVnode.el.nextSibling) oldStartVnode = oldChildren[++oldStartIdx] newEndVnode = newChildren[--newEndIdx] }else if (oldEndVnode.key === newStartVnode.key){ patch(oldEndVnode,newStartVnode,container) insert(oldEndVnode.el,container,oldStartVnode.el) oldEndVnode = oldChildren[--oldEndIdx] newStartVnode = newChildren[++newStartIdx] }else { const idxInOld = oldChildren.findIndex( node => node.key===newStartVnode.key ) if (idxInOld > 0 ){ const vnodeToMove = oldChildren[idxInOld] patch(vnodeToMove,newStartVnode,container) insert(vnodeToMove.el,container,oldStartVnode.el) oldChildren[idxInOld] = undefined }else { patch(null ,newStartVnode,container,oldStartVnode.el) } newStartVnode = newChildren[++newStartIdx] } } if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx){ for (let i = newStartIdx; i<=newEndIdx ; i++){ patch(null ,newChildren[i],container,oldStartVnode.el) } } if (newEndIdx < newStartIdx && oldStartIdx<=oldEndIdx) { for (let i=oldStartIdx;i<oldEndIdx;i++){ unmount(oldChildren[i]) } } } function insert (el,parent,anchor=null ) { parent.insertBefore(el,anchor) }
Vue3快速diff算法 在实测中性能最优,它借鉴了文本Diff中的预处理思路,先处理新旧两组结点中相同的前置结点和相同的后置结点,当前前置结点和后置结点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载已经不存在的结点来完成更新,则需要根据结点的索引关系,构造一个最长递增子序列,source数组,用来存储新的一组子节点在旧子节点中的位置索引,后面用它来计算最长递增子序列,最长递增子序列所指向的结点即为不需要移动的结点
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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 function patchKeyedChildren (n1,n2,container ) { let j = 0 ; let oldVnode = oldChildren[j] let newVnode = newChildren[j] while (oldVnode.key === newVnode.key) { patch(oldVnode,newVnode,container) j++ oldVnode = oldChildren[j] newVnode = newChildren[j] } let oldEnd = oldChildren.length-1 let newEnd = newChildren.length-1 oldVnode = oldChildren[oldEnd] newVnode = newChildren[newEnd] while (oldVnode.key === newVnode.key){ patch(oldVnode,newVnode,container) oldEnd--; newEnd--; oldVnode=oldChildren[oldEnd] newVnode=newChildren[newEnd] } if (j>oldEnd && j<=newEnd) { const anchorIndex = newEnd+1 const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el :null while (j<newEnd){ patch(null ,newChildren[j++],container,anchor) } }else if (j>newEnd&&j<=oldEnd){ while (j<=oldEnd){ unmount(oldChildren[j++]) } }else { const count = newEnd -j+1 const source =newArray(count) source.fill(-1 ) let move =false let pos =0 const oldStart = j const newStart = j const keyIndex = {} for (let i = newStart;i<=newEnd;i++){ keyIndex[newChildren[i].key]=i } let patched = 0 ; for (let i = oldStart;i<=oldEnd;i++){ const oldVnode = oldChildren[i] if (patched<=count){ const k= keyIndex[oldVnode.key] if (typeof key !== "undefined" ){ newVnode = newChildren[k] patch(oldVnode,newVnode,container) patched++; source[k-newStart]=i if (k<pos){ move=true }else { pos=k; } }else { unmount(oldVnode) } }else { unmount(oldVnode) } } if (move){ const seq = lis(sources) let s=seq.length-1 let i=count-1 for (i;i>=0 ;i--){ if (source[i]===-1 ){ const pos=i+newStart const newVnode = newChildren[pos] const newPos=pos+1 const anchor = newPos <newChildren.length?newChildren[newPos].el:null patch(null ,newVnode,container,anchor) }else if (i!==seq[s]){ const pos=i+newStart const newVnode = newChildren[pos] const newPos=pos+1 const anchor = newPos <newChildren.length?newChildren[newPos].el:null insert(newVnode.el,container,anchor) }else { s-- } } } } } function lis (arr ) { let len = arr.length, res = [], dp = new Array (len).fill(1 ); for (let i = 0 ; i < len; i++) { res.push([i]) } for (let i = len - 1 ; i >= 0 ; i--) { let cur = arr[i], nextIndex = undefined ; if (cur === -1 ) continue for (let j = i + 1 ; j < len; j++) { let next = arr[j] if (cur < next) { let max = dp[j] + 1 if (max > dp[i]) { dp[i] = max nextIndex = j } } } if (nextIndex !== undefined ) res[i].push(...res[nextIndex]) } let index = dp.reduce((prev, cur, i, arr ) => cur > arr[prev] ? i : prev, dp.length - 1 ) return result[index] }
时间复杂度: 1.传统diff算法中,通过循环递归对结点进行比较,同层级比较的时候就算使用动态规划(参考编辑距离),时间复杂度最坏为O(n^2),树递归比较的话,复杂度为O(n^3)
2.React的开发者结合Web界面的特点做出了两个大胆的假设,使得Diff算法复杂度直接从O(n^3)降低到O(n),假设如下:
两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构; 对于同一层次的一组子节点,它们可以通过唯一的id进行区分。 通过这两个假设,他们提供了下面的Diff算法思路,diff算法过程中使用优先判断和就地复用策略提高效率
(1).同层比较
新的Diff算法是逐层进行比较,只比较同一层次的节点,大大降低了复杂度
(2).不同类型结点的比较
如果新旧结点类型不同,diff算法会字节删除旧的结点及其子节点并插入新的结点,这是由于前面提出的不同组件产生的DOM结构一般是不同的,所以可以不用浪费时间去比较。注意的是,删除节点意味着彻底销毁该节点,并不会将该节点去与后面的节点相比较。
(3)相同类型结点比较
diff算法更新结点的属性实现转换
(4)列表结点需要给key进行高效比较