面试官又问我 React 中的 Diff 算法?

React 渲染流程

首先我们需要了解React的整体流程:

  • render阶段:计算要渲染的虚拟DOM (调度器Scheduler,协调器Reconciler)
  • commit阶段:渲染UI(渲染器renderer)

如果再展开的细致一些:

  1. 调度器(Scheduler):调度任务,为任务排序优先级,让优先级更高的任务进入Reconciler;
  2. 协调器(Reconciler):生成Fiber对象, 收集副作用,找出哪些节点发生了变化,打上不同的flags(diff 算法就是发生在这个环节);
  3. 渲染器(Renderer):根据协调器计算出虚拟DOM渲染同步的渲染节点到视图上。

Diff 算法

谁和谁在 Diff?

前文我们提过,React的Fiber是双树共存状态:

  • Current Fiber Tree (当前UI对应的Fiber Tree)
  • WorkInProgress Fiber Tree (内存中构建的Fiber Tree)

除了这两个,我们还需要知道一个对象,就是我们写代码的JSX 被render调用之后返回的结果。也就是当前状态下描述的DOM信息。

DIff算法就是Current Fiber Tree 和 JSX 对象 进行对比,生成 WorkInProgress Tree。 生成完之后再进行替换。

生成新的Fiber Tree,如何提升效率呢?那当然是能复用就复用,不能复用的再重新生成。Diff算法的核心就是 复用

React 对 Diff算法 进行的优化

Diff算法本身其实是十分消耗性能的。 两棵树的进行Diff算法时间复杂度为 O(n^3):

  • 每对节点的比较需要遍历其所有子节点的组合。
  • 对于两棵大小为 n 的树,可能的子树对数为 O(n²)
  • 每个子树对的比较需要遍历子节点组合,最坏情况下复杂度为 O(n)
  • 总时间复杂度为 O(n²) × O(n) = O(n³)

所以React 对 Diff算法设置了限制,将复杂度降至O(n):

  • 只对同级别的diff,
  • 不同类型的元素也不会进行diff。 比如div变成了p。 会进行销毁再重建,
  • 开发者通过key来暗示哪些子元素能够保持稳定。 比如两个元素,互换了位置, 如果没有key,就会进行销毁再重建, 如果使用了key,那么此时的DOM元素是可以复用的,直接互换位置。

key的作用

场景 1:无 Key 的列表

React 默认按索引顺序比对子节点。如果列表发生重排或中间插入元素,会导致大量节点被重新创建。

js 复制代码
// 旧列表
<ul>
  <li>A</li>  {/* Index 0 */}
  <li>B</li>  {/* Index 1 */}
</ul>

// 新列表(在头部插入元素)
<ul>
  <li>C</li>  {/* Index 0 */}
  <li>A</li>  {/* Index 1 */}
  <li>B</li>  {/* Index 2 */}
</ul>

React 会发现索引 0 的节点从 A 变为 C,索引 1 的节点从 B 变为 A,最终销毁 AB,创建 CAB,性能极差。

场景 2:有 Key 的列表

通过为子节点添加唯一的 key,React 可以识别节点,从而避免不必要的销毁和重建。

less 复制代码
// 旧列表
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>

// 新列表(插入 C)
<ul>
  <li key="c">C</li>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>

React 会识别出 key="a"key="b" 的节点只是位置变化,仅插入新节点 C,避免重复渲染 AB

单节点diff

单节点指的是新节点是单一节点, 但是旧节点是不一定。

此时判断diff是否能够复用的判断流程:

  • 判断key是否相同, (更新前后如果不设置key, 也是相同的情况)
  • 如果key相同,在判断type是否相同,如果type相同,就复用,如果type不同,无法复用(兄弟元素也不能复用,直接删除)。 如果key不同,直接不能复用(可以再遍历兄弟元素,是否可以复用)。

多节点diff

指新节点是多个,

React团队发现 对节点的更新操作要比 增删 移动 要多。 所以进行多节点diff的时候,React会进行两轮遍历

  • 第一轮遍历会尝试逐个的复用节点。当遇到key不同的时候 无法复用就结束遍历。 如果遇到key相同, 但是type不同, 此时会将这个FiberNode添加到del数组中, 回头统一删除, 根据新的react元素创建一个新元素, 到那时遍历没有结束。 继续遍历 等到结尾, 或者等key 或者 type 不同 就会结束
  • 第二轮遍历处理上一轮遍历中没有处理完的节点。 如果第一轮提前结束了, 说明没有JSX没有遍历完,或者CurrentFiberNode没有遍历完。 有三种情况, JSX遍历完了 到那时CurrentFiberNode没有遍历完,说明有需要被删除的节点。 第二种情况, CurrentFIber遍历完成了, 但是JSX没有遍历完 ,说明新增了,, 第三种情况下, 都有剩余, 将剩余的currentFibernode加入到map里面, 然后遍历JSX取再map中是否有能服用的, 找不到就新增。 遍历完之后, map中还有剩余 就将他们删掉。

总结

面试官问到diff算法的时候, 我们需要回答谁和谁Diff,diff的目的是什么,diff的核心是什么,key的作用是什么。

相关推荐
ObjectX前端实验室5 分钟前
【react18原理探究实践】异步可中断 & 时间分片
前端·react.js
SoaringHeart8 分钟前
Flutter进阶:自定义一个 json 转 model 工具
前端·flutter·dart
努力打怪升级10 分钟前
Rocky Linux 8 远程管理配置指南(宿主机 VNC + KVM 虚拟机 VNC)
前端·chrome
brzhang38 分钟前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang43 分钟前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
reembarkation1 小时前
自定义分页控件,只显示当前页码的前后N页
开发语言·前端·javascript
gerrgwg1 小时前
React Hooks入门
前端·javascript·react.js
ObjectX前端实验室1 小时前
【react18原理探究实践】调度机制之注册任务
前端·react.js
汉字萌萌哒2 小时前
【 HTML基础知识】
前端·javascript·windows
ObjectX前端实验室2 小时前
【React 原理探究实践】root.render 干了啥?——深入 render 函数
前端·react.js