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

相关推荐
augenstern41641 分钟前
webpack重构优化
前端·webpack·重构
海拥✘1 小时前
CodeBuddy终极测评:中国版Cursor的开发革命(含安装指南+HTML游戏实战)
前端·游戏·html
寧笙(Lycode)1 小时前
React系列——HOC高阶组件的封装与使用
前端·react.js·前端框架
asqq81 小时前
CSS 中的 ::before 和 ::after 伪元素
前端·css
拖孩2 小时前
【Nova UI】十五、打造组件库之滚动条组件(上):滚动条组件的起步与进阶
前端·javascript·css·vue.js·ui组件库
Hejjon2 小时前
Vue2 elementUI 二次封装命令式表单弹框组件
前端·vue.js
小堃学编程3 小时前
前端学习(3)—— CSS实现热搜榜
前端·学习
Wannaer3 小时前
从 Vue3 回望 Vue2:响应式的内核革命
前端·javascript·vue.js
不灭锦鲤3 小时前
xss-labs靶场基础8-10关(记录学习)
前端·学习·xss
Bl_a_ck3 小时前
--openssl-legacy-provider is not allowed in NODE_OPTIONS 报错的处理方式
开发语言·前端·web安全·网络安全·前端框架·ssl