面试官又问我 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的作用是什么。

相关推荐
Aotman_3 分钟前
ES6 Object.values 特定字段处理
前端·javascript·ecmascript·es6
wordbaby3 分钟前
前端架构入门:测试策略
前端
骑着小黑马4 分钟前
前端程序员自己的知识库,使用NodeJS+LLM搭建一个属于自己的知识库
前端·人工智能
wordbaby6 分钟前
加速 Web 应用:资源压缩详解与 Vite + Nginx 实践指南
前端·nginx·vite
宝耶22 分钟前
HTML:表格数据展示区
前端·html
程序员海军36 分钟前
一键把网站变成吉卜力风格的神器来了
前端·chatgpt
三原37 分钟前
前端微应用-乾坤(qiankun)原理分析-沙箱隔离(js)
前端·架构·前端框架
IT专家-大狗39 分钟前
Edge浏览器安卓版流畅度与广告拦截功能评测【不卡还净】
android·前端·edge
Kx…………1 小时前
Day3:个人中心页面布局前端项目uniapp壁纸实战
前端·学习·uni-app·实战·项目
肠胃炎1 小时前
认识Vue
前端·javascript·vue.js