React 在每次更新的时候,都会重新进行调用一次 render
方法,生成一颗新的 virtual dom 树。React会将这颗新的树与老的树做一个对比,以找出一个最少更新 dom 操作的方式。这个新老树对比的算法就是鼎鼎大名的 diff 算法。
React diff 算法依托于 virtual dom。virtual dom 是对真实的 dom 的抽象,在 React 更新的时候我们可以根据 virtual dom 直接渲染成真实的dom,这样的就与直接的操作dom无异。diff 算法本质上是提供了两颗树的最小转化路径,相当于在操作真实 dom 之前,对各个节点一次比较判断,只做必要的 dom 做操作。
diff 算法有很多,复杂度大致都在 O(n^3),React 根据前端场景,忽略了跨层级节点的比较,只对同一层级内的节点进行比较,且同一层级内的节点都有自己唯一的标识,这样所有节点遍历一次就能完成 diff 操作,将 diff 算的复杂度降到了可以接受的程度 O(n)
节点标识
每一个 virtual dom 节点都有自己独特的标识,用来在更新中确认标识是否存在,如果存在说明该节点可以复用,如果不存在,就将该节点删除,新增节点。
标识由两部分组成,一部分是节点自身在父节点下的 index 值,另一部分是由用户传入的 key 值,如果用户没有传入 key 值,则标识仅由 index 值来确定。
对于由 list 动态渲染出来的子元素,用来比较的范围是当前 list 之内的所有子元素,因此标识还加上了自身的一个 index。
节点标识的具体规则如下:
// 非 list 子元素
`.${key?'$'+key:index}`
// list子元素
`.${index}:${key?'$'+key:subIndex}`
diff 过程
针对单个 dom 节点的操作总共有三种类型,分别是删除、新增、以及更新(属性、位置)。
如上图所示,更新前后这一层级的节点变化为:
A/B/C/D/E 节点进行了更新操作。新增 G 节点,删除了 F 节点。
遍历新节点,完成新增、更新的标记
更新
如果在旧节点中发现了相同标识的节点,就对该节点进行更新操作,并且判断该节点是否需要进行移动。
每次在组件挂载和更新后都会记录节点的挂载顺序,这样在下次更新中就可以对节点的相对顺序进行判断。
因为我们对新节点的遍历顺序是从前往后,所以对于移动的操作 只有后移,没有前移 。
发生后移的判断条件为当前的节点的挂载 index 小于已处理节点的 index,说明前后顺序发生了变化。
新增
如果没有发现相同标识的节点,说明该节点为新增节点。
遍历旧节点,完成删除的标记
遍历一遍旧节点,如果某个节点的标识没有出现在新的节点中,说明该节点需要删除。
最终,对收集到的操作标记进行统一处理。