深入理解React渲染阶段中的Diff算法与复用策略

11/24/2023 React

React 在 render 阶段采用 Diff 算法来比较新旧虚拟 DOM 树,以实现高效的更新。本文将深入解析 React 的 Diff 算法,并重点关注其三种复用策略,帮助开发者更好地优化 React 应用性能。

React 的 Diff 算法主要采用三种策略:

  • 同层级节点比较
    React 只会比较同层级的节点,对于跨层级的节点会忽略不计。 diff-1
    上图所示,在节点 A 的更新中,React 会删除 A 及其子节点 B、C,为 D 子节点创建子节点 A,B,C
  • 不同类型元素的处理
    当两个元素类型不同时,React 会销毁旧元素及其子孙节点,并创建新元素及其子孙节点。如果元素由 div 变为 p,React 会销毁 div 及其子孙节点,并新建 p 及其子孙节点。
    diff-2.png
    虽然 D 跟 G 的子树是相似的,但是因为 D 和 G 是两个不同的节点,那么 React 就会把 D 和其子节点删除后,再重新生成 G,E,F 节点
  • 使用 key prop 进行稳定标记
    通过 key prop 可以暗示哪些子元素在不同渲染下能保持稳定。对于单节点复用和多节点复用,复用策略都是先比较 key,然后再比较类型。
    如果新旧都没有指定 key,那么 key 就是 null。

单节点复用

  1. 如果节点的 key 相同但 type 不同,将该节点及其兄弟节点的 Fiber 全部标记删除。
  2. 如果节点的 key 不同,将该节点标记删除,并遍历其兄弟节点。

多节点复用

  1. 主要判断节点是新增,删除和更新操作
  2. React 会进行两轮遍历
    a. 第一轮遍历的作用: 处理需要更新的节点
    b. 第二轮遍历的作用: 处理剩下的不属于更新的节点
  3. 进行第一轮遍历,直到 newChildren 或者 oldFiber 遍历完,或者满足第 c 点。
    a. 如果 key 相同,**type相同,进行复用,继续遍历
    b. 如果 key 相同,
    type**不相同,该 fiber 节点标记删除,继续遍历
    c. 如果 key 不同,跳出遍历
  4. 进行第二轮遍历
    a. 如果 newChildrenoldFiber 遍历完,皆大欢喜,最完美情况
    b. 如果 newChildren 遍历完, oldFiber 没有, oldFiber 全部标记删除
    c. 如果 newChildren 没有遍历完, oldFiber 遍历完, newChildren 全部标记添加
    d. 如果都没有遍历完,进入核心部分
  5. 会以 key - fiber 的形式将 oldFiber 保存在 map 中,然后遍历 newChildren,以最后一个可复用的节点为参照物,用参照物 index 和 oldFiber 的 index 进行判断是否移动了。

diff-3.png

对于 A 节点来说,以 B 为参照物,标记移动了, D,C 同理。

diff-4.png

对于 ABC 节点来说,以 D 为参照物,标记移动了。

简单来说,会固定后面的复用节点,前面的节点就会被标记移动。

结论

为了优化性能,尽量减少将节点从后面移动到前面的操作,特别是在涉及大量节点的列表渲染中。