一、diff算法的简史
- diff 算法并不是近年才有的,早在多年以前就已经有人在研究 diff 算法了,最早复杂度基本是 O(m^3n^3)然后优化了 30 多年,终于在 2011 年把复杂度降低到 O(n^3) 这里的 n 指的是节点总数
- 所以 1000 个节点,要进行 1亿次操作!
- 当今的 diff 算法主要指 react 横空出世之后提出的近代同层比较的 diff 算法,因为是同层嘛,复杂度就到了 O(n)
二、为什么需要 diff?
- 本质上就是为了性能
- 直接操作dom容易慢;DOM对象本身也是一个js对象,所以严格来说,并不是操作这个对象慢,而是说操作了这个对象后,会触发一些浏览器行为,比如布局(layout)和绘制(paint)
- 使用 “数据模型 -> virtual dom -> 视图(DOM)”思路去提升性
三、diff算法如何进行比较
-
计算机世界里,比较两棵树的异同,其实可以抽象的理解为,将一颗树转成另一颗树的过程,这里的复杂度需要衡量的是‘’操作数“
-
假设,将字符串 'hello' -> 'hallo' 需要几步?一眼望去就知道答案是一步,这个时候脑子里的思考过程其实就是编辑距离算法的模型了
-
抽象一点,对字符串的操作不外乎三种,「替换」「插入」「删除」,执行这三种操作后到达目的的最小操作数,就是最短编辑距离,这里的复杂度就是我们需要考虑的,这就是‘’操作数‘’
-
树的最短编辑距离算法复杂度是 O(n^2),其实就是实现的时候,需要双层 for 循环去计算,这里复杂度就是 O(n^2) 了,然后,因为 diff 还要做一次 patch,(找到差异后还要计算最小转换方式),这个时候还要在之前遍历的基础上再遍历一次,所以累计起来就是 O(n^3)
-
const arr = [a, b, c] const newArr = [b, d, e, f] // 两个数组进行同层比较 // [a, b] [b, d] [c, e] [null, f]// 具体到虚拟DOM for (let i = 0, len = oldNodes.length; i < len; i++) { if (oldNodes[i].type !== newNodes[i].type) { replace() } else if (oldNodes[i].children && oldNodes[i].children.length) { // 如果没有这一层,假设 type 全不相同,那么就是 O(n),最坏复杂度 O(nm) } }
四、降低复杂度
- 将复杂度降下来,其实就是在算法复杂度、虚拟 dom 渲染机制、性能中找了一个平衡,react 采用了启发式的算法,做了如下最优假设:
- 如果节点类型相同,那么以该节点为根节点的 tree 结构,大概率是相同的,所以如果类型不同,可以直接「删除」原节点,「插入」新节点
- 跨层级移动子 tree 结构的情况比较少见,或者可以培养用户使用习惯来规避这种情况,遇到这种情况同样是采用先「删除」再「插入」的方式,这样就避免了跨层级移动
- 同一层级的子元素,可以通过 key 来缓存实例,然后根据算法采取「插入」「删除」「移动」的操作,尽量复用,减少性能开销
- 完全相同的节点,其虚拟 dom 也是完全一致的
-
基于这些假设,可以将 diff 抽象成只需要做同层比较的算法,这样复杂度就直线降低了
-
针对于子元素的key。如果没有指定 key 的话,react 是会给 warning 的,同时默认使用下标 index 作为 key,但是这其实是不建议的。
-
key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes
-
如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
-
举个例子,假设原来有 [1, 2, 3] 三个子节点渲染了,假设我们这么操作了一波,将顺序打乱变成 [3, 1, 2],并且删除了最后一个,变成 [3, 1];最优的 diff 思路应该是复用 3, 1组件,移动一下位置,去掉 2 组件,这样整体是开销最小的,如果有 key 的话,这波操作水到渠成,如果没有 key 的话,那么就要多一些操作了:
- 判断哪些可以复用,有 key 只需要从映射中检查3, 1在不在,没有 key 的话,可能就执行替换了,肯定比「复用」「移动」开销大了
- 删除了哪一个?新增了哪一个?有 key 的话是很好判断,之前的映射没有的 key,比如变成 [3, 1, 4]那这个 4 很容易判断出应该是新建的,删除也同理。但是没有 key 的话就麻烦一些了。
- 那既然 key 很重要,那么就有两种操作是误区了
- 第一是使用随机数,这种导致的问题肯定就是每次 key 都不一样,那这样就不是复用了,都是新建了。
- 第二种是使用数组下标,就会带来一些问题,看看这个 codesandbox.io/s/ancient-m…
-
我们来分析一下:[1, 2, 3] 这是原来的渲染节点,页面展示出 1, 2, 3,然后我们 splice(0, 1) 删除第一个元素后,理想情况是变成 [2, 3],但是因为使用了下标为 key,对比前后两次 keys,[0, 1, 2] -> [0, 1] 因为 vue 的机制,sameNode 判断一波后,误认为是 2 被删除了!
function sameVnode (a, b) { return ( a.key === b.key && // key值 a.tag === b.tag && // 标签名 a.isComment === b.isComment && // 是否为注释节点 // 是否都定义了data,data包含一些具体信息,例如onclick , style isDef(a.data) === isDef(b.data) && sameInputType(a, b) // 当标签是的时候,type必须相同 ) }
-
react ,例子:codesandbox.io/s/fervent-n…
-
react正确的删除了第一个元素,但是其他的元素都被重新渲染力,使用性能的开销来换取正确的解。
五、虚拟DOM
1. 什么是虚拟 DOM
{
type: 'div',
props: {
children: []
},
el: xxxx
}
2. 怎么创建虚拟 DOM
// h 、createElement...
function h(type, props){
return { type, props }
}
3. 使用
// JSX:
<div>
<ul className='padding-20'>
<li key='li-01'>this is li 01</li>
</ul>
</div>
// 经过一些工具转一下:
createElement('div', {
children: [
createElement('ul', { className: 'padding-20' },
createElement('li', { key: 'li-01'}, 'this is li 01'))
]
})
4.虚拟DOM的数据结构有了,那就是渲染了 (mount/render)
//f(vnode) -> view
f(vode) {
document.createElement();
....
parent.insert()
. insertBefore
}
export const render = (vnode, parent) => { }
<div id='app'></div>
5.diff 相关了(patch)
f(oldVnodeTree, newVnodeTree, parent) -> 调度? -> view