包含:Fiber 节点结构(tag/stateNode) 、 调和(reconciliation)过程(双缓冲) 、 优先级调度(Lane)
要求: 看 React 源码注释版,画 Fiber 节点结构图
一、虚拟 DOM
真实操作真实 DOM 之所以效率低下,并非因为 JavaScript 执行慢,而是因为每次修改 DOM 都可能触发浏览器昂贵的熏染流程,包括重新计算样式(Recalculate Style)、布局重排(Reflow)、重绘(Repaint)。频繁的直接操作 DOM,尤其在大型应用中,很容易导致页面卡顿。
虚拟 DOM 通过引入一个轻量级的 JavaScript 对象层,巧妙的解决了这个问题,其核心价值体现在:
- 性能下限的保障 : 虚拟 DOM 允许 React 现在内存中创建一个新的虚拟 DOM 树,然后通过高效的 Diff 算法与旧的虚拟 DOM 树进行比较,计算出最小的差异。最后,React 会将这些差异批量应用到真实 DOM 上。这个过程将多次、零散的 DOM 操作合并成一次新的高效的更新,极大地减少触发浏览器重排和重绘的次数,从而保证了应用的性能不会太差
- 声明式编程与开发效率: 虚拟 DOM 让开发者可以使用声明式的方式来编写 UI。只需关心应用的状态(State),当状态改变时,React 会自动处理 UI 的更新,而无需手动编写复杂的 DOM 操作代码(如
appendChild、removeChild等)。这大大简化了开发流程,降低代码的复杂度和出错的可能性 - 跨平台能力 : 虚拟 DOM 本质只是一个描述 UI 结构的 JavaScript 对象,它与具体的渲染平台无瓜。这使得 React 可以通过渲染不同的渲染器(Renderer)将同一套组件逻辑渲染到不同的目标
- Web 端 : 通过 ReactDom 渲染成浏览器 DOM
- 移动端 : 通过 React Native 渲染成 IOS 或 Android 的原生组件
- 服务端 : 通过服务端渲染 (SSR)生成 HTML 字符串
1. 虚拟 DOM 的结构组成
虚拟 DOM 在内存中就是一个普通的 JavaScript 对象,通常被称为 VNode 。当在 React 编写 JSX 的时候,它会在编译阶段被转换成 React.createElement() (旧版模式,当前是 jsxRuntime.jsx() )的调用,这个函数最终返回一个 VNode 对象。
const element = {
// 核心安全标识,用于防止 XSS 攻击
$$typeof: Symbol.for('react.element'),
// 元素的类型,可以是字符串(如 'div')或一个组件
type: 'div',
// 元素的 key 属性,用于在 Diff 算法中优化列表的更新
key: 'my-key',
// 元素的 ref 属性,用户获取真实的 DOM 节点的引用
ref: null,
// 包含的所有属性(props)
props: {
id: 'app',
className: 'container',
children: 'Hello world',
// ....
},
// 记录创建该元素的组件
_owner: null,
};
其中, $$typeof 是一个 Symbol 类型的值。由于 JSON 不支持 Symbol ,这个设计可以有效的防止跨站脚本攻击(XSS)。如果一个恶意脚本从服务器返回一个 JSON 对象来模拟 React 元素,由于缺少有效的 $$typeof 属性, React 会拒绝渲染它。
二、Diff 算法
虚拟 DOM 的高效性依赖于 Diff 算法。这是个启发式算法,通过一些策略将两课树对比的复杂度从 O(n³) 降低到 O(n),使其在实际应用中非常高效。
- 分层比较(Tree Diff) : React 只对同一层级的节点进行比较。如果在一个节点不同层级间移动, React 会直接销毁旧节点并创建新节点,而不是尝试移动它
- 类型检查(Component Diff) : 如果两个节点的类型不同(例如从
<div>变成了<p>,或从组件 A 变成了组件 B ), React 会直接销毁旧节点并会认为他们产生的树结构完全不同,因此会替掉整个子树 - Key 标识(Element Diff) : 对于同一层级的子元素列表,
key属性至关重要。它帮助 React 识别哪些元素是新增的、删除的,或是仅仅移动了位置,从而实现对 DOM 节点的最大化复用,避免不必要的重建
1. 与 vue Diff 的区别
React 和 Vue 的 Diff 算法虽然目标一致---高效的更新视图,但它们的实现策略和核心思想存在显著差异。简单来说, React 的 Diff 更依赖于运行时的启发式策略,而 Vue 这利用编译信息来优化运行时的 Diff 过程。
1.1 优化策略
这是两者最根本的理念差异。
- React :纯运行时优化 React 的 Diff 算法完全在运行时进行。当组件状态更新时, React 会重新执行组件的 render 函数,生成一颗新的虚拟 DOM 树, 然后与旧的树进行对比。React 无法预先知道哪些部分是静态的,哪些是动态的,因此它需要对整个树的输出进行“盲”比较
- Vue :编译时 + 运行时优化 Vue (尤其是 Vue 3 )在编译阶段就对模版进行了静态分析,并生成了带有优化信息的渲染函数
-
- 静态提升( Static Hoisting ):对于模版中不会变化的静态节点, Vue 会在组件初始化时创建一次,并在后续的渲染中直接复用,完全跳过 Diff 过程
-
- 补丁标识( Patch Flags ):对于动态节点, Vue 会在编译时给它打上标记( Patch Flag ),明确指出这个节点的文本会变,还是类名会变等。在 Diff 时, Vue 会直接跳过静态节点,只针对带有 Patch Flag 的动态节点进行精确的、字段节点的更新
-
结论: Vue 将大量优化工作前置到编译阶段,使得运行时 Diff 过程更加精准和高效。而 React 则依赖于开发者手动使用 React.memo、useMemo 等方式进行编译(万恶的 React 居然试图使用 react-compiler 来追赶先进的东方大国开发的遥遥领先的 Vue ,甚至还说自己的 React 通常在没有优化的情况下已经足够快,可恶 😠😠)
1.2 列表 Diff 算法
在处理子元素列表(如 ul > li )的更新时,两者的算法差异最为明显。
- React :单向遍历 + Key 映射 React 采用的从左到右的单向遍历策略。他会同时遍历新旧两个列表,通过
key来寻找可复用的节点- 流程:对比新旧列表相同的索引位置的节点。如果
key相同且类型一致么,则复用;否则,标记为更新或替换 - 特点:算法简单直接。但如果列表发生乱序(如
[A, B ,C]变成[C, A , B]), React 可能会进行较多的不必要操作,因为它无法很好的识别出这是整体移动
- 流程:对比新旧列表相同的索引位置的节点。如果
- Vue :双端比较 + 最长递增子序列(LIS) Vue 3 采用了更复杂的 “四指针”或“双端比较”算法,并引进了 最长递增子序列( LIS ) 来优化节点的移动
- 流程:
-
- 双端比较 :从新旧列表的头尾两端同时向中间对比(头对头、尾对尾、头对尾、尾对头),快速处理首尾相同或位置简单的节点
-
- LIS 优化 :当双端比较无法匹配时,说明中间部分发生了复杂的移动。Vue 会利用 LIS 算法,找出新旧列表中最长顺序稳定的子序列。这部分节点被认为是“最稳定”的,不需要移动,然后,只对其他节点进行插入、删除或移动操作
-
- 特点:算法更智能,能以最少的 DOM 操作来处理复杂的列表变动个,尤其子啊列表乱序是优势明显
- 流程:
Vue 的列表 Diff 算法在处理复杂变动时更高效,能最大限度的复用和移动现有 DOM 节点。 React 的算法则更为简单,对 key 的稳定性依赖更强
1.3 key 的依赖程度
虽然两者都是用 key 来优化列表渲染,但依赖程度不同。
- React :强关联
key在 React 中至关重要。如果列表没有提供key或key不稳定(如使用数组索引), React 的 Diff 算法效率会急剧下降,可能导致组件状态错乱和不必要的完整重建 - Vue :优化但不完全依赖 Vue 同样推荐使用稳定
key来获取最佳性能。但由于其双端比较和 LIS 算法的存在,即使在某些没有key的简单场景下, Vue 也能通过节点类型的对比做出相对合理的更新决策,表现更“宽容”一些
三、Fiber 架构
在 React 16 之后,虚拟 DOM 的协调过程由新的Fiber 结构 负责。Fiber 将虚拟 DOM 的 Diff 和更新过程拆解成许多的小的、可中断的任务单元。这使得 React 可以利用浏览器的空闲时间来执行渲染任务,避免了长时间的计算阻塞主线程,从而实现了更流程的用户体验和并发渲染等高级特性。
有一部分内容在生命周期中进行了讨论