Appearance
React 原理篇
问题 1:虚拟 DOM 的意义
优点:
- 减少实际的 DOM 操作(提高性能):通过比较新旧虚拟 DOM 树的差异,React 可以确定需要更新的部分,并生成最小化的 DOM 操作序列。这样可以减少实际的 DOM 操作次数,提高性能。
- 批量更新:React 会将所有需要更新的 DOM 操作批量执行,从而避免了频繁的 DOM 操作,提高了性能。
- 跨平台兼容性:虚拟 DOM 是一个轻量级的 JavaScript 对象,可以在不同的平台上运行,例如浏览器、移动设备和服务器。这使得 React 可以在多个环境中使用相同的代码和逻辑。
- 更好的开发体验(简化开发):虚拟 DOM 使得开发者可以使用类似于 HTML 的标记语言来描述 UI,而不需要直接操作 DOM。这简化了开发过程,并提供了更好的开发体验。
缺点:
- 内存消耗:虽然虚拟 DOM 提高了渲染效率,但是它需要额外的内存空间来存储虚拟 DOM 树及其状态,对于大规模的应用(例如 3D 可视化等)或资源受限的设备来说,可能会成为一个负担。
- 初次渲染延迟:首次加载应用时,构建虚拟 DOM 树并将其转换为真实 DOM 的过程会增加一定的初始化时间,导致首屏加载速度变慢。
- 复杂性增加:对于一些简单的应用场景,引入虚拟 DOM 机制可能反而增加了不必要的复杂度。此外,理解虚拟 DOM 的工作原理以及如何高效地使用相关框架,也需要一定的学习成本。
问题 2:JSX
JSX 转为真实 DOM 的过程?(分首次渲染 + 更新阶段)
Details
虚拟 DOM:js 对象,用来描述 DOM 结构的
AST:js 对象,用来描述代码结构的
DSL(数据协议设计):js 对象,用来描述特定领域的业务数据(例如:低代码平台的 json schema,vue、react 的 vnode)
阶段 1:首次渲染(页面第一次显示)
- 使用 Babel 将 jsx 转为 React.createElement 后
React.createElement(type, props, ...children)
js
// 编译后(React 17 之前)
React.createElement(
'div',
{ className: 'box' },
React.createElement(Header, null, 'hello'),
React.createElement('div', null, 'container'),
'footer'
)
// 编译后(React 17+,自动引入 _jsx)
import { jsx as _jsx } from 'react/jsx-runtime'
const element = _jsx('h1', { className: 'title', children: 'Hello, world!' })- 创建虚拟 DOM
React.createElement 返回一个描述 UI 的 JavaScript 对象(即虚拟 DOM)。
- 调用渲染入口 render
通过 ReactDOM.render(React 18 前)或 createRoot.render(React 18 后),告诉 react 将 VNode 插入到容器中
render本质:告诉 React “我要把这个 VNode 描述的 UI 渲染到容器里”,而 React 会在这个调用之后,自动完成 “VNode 转真实 DOM + 插入容器” 的工作
- 递归解析 VNode,生成真实 DOM
React 会遍历根 VNode 及其子 VNode,递归执行:
- 若 VNode 的 type 是「原生标签」(如 div):调用 document.createElement('div') 创建真实 DOM 元素,再给元素设置 props(如 className);
- 若 VNode 的 type 是「组件」(如 Header):执行组件函数 / 类组件的 render 方法,获取组件返回的 VNode,再递归解析(直到拿到原生标签的 VNode);
- 若子元素是「文本节点」(如 'footer'):调用 document.createTextNode('footer') 创建真实文本节点。
- 挂载真实 DOM 到容器
所有递归生成的真实 DOM 元素,会按 VNode 的结构组装成 DOM 树,最终插入到 #root 容器中,页面首次渲染完成。
阶段 2:更新阶段
- 重新生成新的 VNode
状态变化后,组件会重新执行渲染函数(返回新的 JSX),经过 Babel 编译后,生成「新的虚拟 DOM 树」(新 VNode)。
- Diff 算法对比新旧 VNode
React 会用 Diff 算法(树 Diff + 列表 Diff + 属性 Diff)对比「旧 VNode 树」(更新前的虚拟 DOM)和「新 VNode 树」:
- 树 Diff:按 “层级遍历”,只对比同一层级的 VNode(不跨层级对比,提高效率);
- 列表 Diff:通过 key 标识列表项,快速找到新增、删除、移动的元素;
- 属性 Diff:对比 VNode 的 props,只更新变化的属性(如 className 变了才修改,不变则跳过)。
👉 核心目的:找到「最小化的 DOM 操作」(如只修改某个元素的文本,而非重建整个 DOM 树)。
- 批量更新优化
React 会将 Diff 找到的所有 DOM 操作「批量收集」,再一次性执行(而非逐个操作 DOM)。原因是:真实 DOM 操作是 “昂贵的”(会触发回流重绘),批量执行能减少回流重绘次数,提升性能。
- React 18 后,默认开启「自动批处理」,即使在异步操作(如 setTimeout、fetch 回调)中更新状态,也会批量处理;
- React 17 及以前,只有同步操作中的状态更新会批量处理。
- 应用 DOM 操作到真实 DOM
批量执行收集的 DOM 操作(如修改元素属性、新增 / 删除 DOM 节点、修改文本内容),真实 DOM 随之更新,页面呈现最新的 UI。
总结
- 首次渲染:JSX → 编译为 _jsx 调用 → 生成 VNode → render 触发 → 递归生成真实 DOM → 挂载容器;
- 更新阶段:状态变化 → 重新生成 JSX → 新 VNode → Diff(新 vs 旧 VNode)→ 批量 DOM 操作 → 真实 DOM 更新。
JSX 循环为何使用 key ?
- 元素的高效识别与复用
React 通过 key 唯一标识列表中的每个元素。当列表发生变化(增删改排序)时,React 会通过 key 快速判断:
- 哪些元素是新增的(需要创建新 DOM 节点)
- 哪些元素是移除的(需要销毁旧 DOM 节点)
- 哪些元素是移动的(直接复用现有 DOM 节点,仅调整顺序)
如果没有 key,React 会默认使用数组索引(index)作为标识,这在动态列表中会导致 性能下降 或 状态错误。
- 避免状态混乱
如果列表项是 有状态的组件(比如输入框、勾选框等),错误的 key 会导致状态与错误的内容绑定。例如:
jsx
// 如果初始列表是 [A, B],用索引 index 作为 key:
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
// 在头部插入新元素变为 [C, A, B] 时:
// React 会认为 key=0 → C(重新创建)
// key=1 → A(复用原 key=0 的 DOM,但状态可能残留)
// 此时,原本属于 A 的输入框状态可能会错误地出现在 C 中。- 提升渲染性能
通过唯一且稳定的 key(如数据 ID),React 可以精准判断如何复用 DOM 节点。如果使用随机数或索引,每次渲染都会强制重新创建所有元素,导致性能浪费。
问题 3:React DOM 的 diff 算法
diff 算法
React 的虚拟 DOM diff 算法是一种用于比较新旧虚拟 DOM 树的差异的算法,目标是找出需要更新的部分,并生成一个最小化的 DOM 操作序列:
- 同层比较,不跨层级
React diff 只在同一层级的虚拟 DOM 节点间对比,不会跨层级查找差异。如果父节点类型变化,直接销毁该节点下所有子树并重建,不深入子节点对比。
- 节点类型判断
- 若新旧节点类型不同(如 div 换成 p),直接替换旧节点及所有子节点,无需后续对比。
- 若节点类型相同,进入属性和子节点的对比阶段。
- 属性对比
- 对比节点的 props 对象,找出新增、删除或修改的属性。
- 对于样式(style)等特殊属性,会进一步对比具体键值对,仅更新变化的部分,而非全量替换。
- 子节点对比(关键优化点)
- 无 key 时:按索引顺序逐一对比,若子节点数量变化或顺序调整,会导致大量节点误更新(效率低)。
- 有 key 时:key 作为节点唯一标识,React 会先通过 key 匹配新旧子节点,仅对匹配成功的节点对比属性/内容,未匹配的节点执行删除/插入操作,大幅减少无效更新。
- 生成并执行最小操作序列 对比完成后,收集所有差异(属性更新、节点插入/删除/移动),生成最小化 DOM 操作指令,React 会批量执行这些指令,避免频繁操作真实 DOM。
react diff 和 vue diff 的区别
静态优化机制:
- Vue 的编译时优化(静态节点标记):模板中的静态节点(无响应式绑定)会被编译为常量,跳过 diff,以及预字符串化。
vue 模版编译优化
虚拟节点静态标记(Patch Flag):更新类型标记,去标注要变化什么
- Vue 3 引入了 Patch Flag,这是一种优化技术,它在编译时标记虚拟节点的动态部分。这样在组件更新时,Vue 只需要关注这些被标记的部分,而不是整个组件树,从而显著提升了性能。
对于单个有动态绑定的元素来说,我们可以在编译时推断出大量信息:
html<!-- 仅含 class 绑定 --> <div :class="{ active }"></div> <!-- 仅含 id 和 value 绑定 --> <input :id="id" :value="value" /> <!-- 仅含文本子节点 --> <div>{{ dynamic }}</div>在为这些元素生成渲染函数时,Vue 在 vnode 创建调用中直接编码了每个元素所需的更新类型:
jscreateElementVNode( 'div', { class: _normalizeClass({ active: _ctx.active }) }, null, 2 /* CLASS */ )最后这个参数
2就是一个更新类型标记 (patch flag)。一个元素可以有多个更新类型标记,会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记,确定相应的更新操作:jsif (vnode.patchFlag & PatchFlags.CLASS /* 2 */) { // 更新节点的 CSS class }内容在官网中有详细介绍:https://cn.vuejs.org/guide/extras/rendering-mechanism.html#patch-flags
缓存静态内容:
html
<div>
<!-- 需缓存 -->
<div>foo</div>
<!-- 需缓存 -->
<div>bar</div>
<div>{{ dynamic }}</div>
</div>
foo和bar这两个 div 是完全静态的,会完全跳过对它们的差异比对。
- 预字符串化:当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。
会把静态内容提前转换成字符串。
html
<div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div>{{ dynamic }}</div>
</div>js
import {
createElementVNode as _createElementVNode,
toDisplayString as _toDisplayString,
createStaticVNode as _createStaticVNode,
openBlock as _openBlock,
createElementBlock as _createElementBlock
} from 'vue'
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock('div', null, [
_cache[0] ||
(_cache[0] = _createStaticVNode(
'<div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div>',
5
)),
_createElementVNode(
'div',
null,
_toDisplayString(_ctx.dynamic),
1 /* TEXT */
)
])
)
}- React 的运行时优化(手动控制更新): 需通过 React.memo、shouldComponentUpdate 或 useMemo 避免无效渲染
优化的设计方向:react 是运行时优化(Fiber 调度),vue 是编译时优化(模板静态分析)
问题 4:Fiber 架构
Fiber 是什么
Fiber 是 React 中一种新的架构,用于实现可中断、可恢复的虚拟 DOM diff 过程。它通过将 diff 过程分为多个小任务,并根据优先级动态地调整任务的执行顺序,解决长任务阻塞主线程导致的页面卡顿问题。
在 React 的旧版本中,虚拟 DOM diff 过程是一个递归的过程,它会一直执行直到完成,期间无法中断。这可能会导致长时间的 JavaScript 执行,从而阻塞主线程,造成页面的卡顿和不流畅的用户体验。
Fiber 优势
- 优先级调度:React 为不同任务分配优先级(如用户输入 > 动画 > 普通更新),通过调度器(Scheduler)实现任务的中断与切换,高优先级可以打断正在执行的低优先级任务。
调度器:负责管理任务的优先级,确保高优先级的任务能够及时执行。
- 可中断的渲染:Fiber 树可以被中断和恢复,这意味着在执行 Fiber 树的 diff 过程时,可以在任意时刻中断当前任务,并优先执行其他任务。Fiber 允许将大的渲染任务拆分成多个小的工作单元(Unit of Work),使得 React 可以在空闲时间执行这些小任务(利用
requestIdleCallback思想)。当浏览器需要处理更高优先级的任务时(如用户输入、动画),可以暂停渲染,先处理这些任务,然后再恢复未完成的渲染工作。
通过检索
flags与subtreeFlags来检测该 fiber 是不是渲染完成了。
flags字段用于标记当前 Fiber 节点的副作用(side effects),例如插入、删除、更新等。
subtreeFlags字段用于标记当前 Fiber 节点的子树中存在的副作用。如果一个 Fiber 节点的 flags 和 subtreeFlags 字段都没有设置副作用标记,说明该节点及其子树已经渲染完毕。
flags 和 subtreeFlags 的具体作用,以及如何判断渲染完成的
要更具体地理解,我们可以结合 React Fiber 的源码逻辑和示例来分析:
一、先明确「副作用(Side Effects)」的类型
在 React 中,flags(在源码中也叫 EffectTag)有很多枚举值,比如:
NoFlags:无标记,表示该节点没有需要处理的副作用。Placement:需要将节点插入到 DOM。Update:需要更新节点的属性、内容。Deletion:需要从 DOM 中删除节点。ChildDeletion:子节点需要被删除。- 还有
Passive(用于处理 useEffect 等副作用)等。
二、flags 和 subtreeFlags 在 Fiber 节点中的结构
Fiber 节点的简化结构类似这样:
javascript
function FiberNode(tag, pendingProps, key) {
// 其他属性...
this.flags = NoFlags // 初始为“无标记”
this.subtreeFlags = NoFlags // 初始为“无标记”
this.child = null // 子 Fiber 节点
this.sibling = null // 兄弟 Fiber 节点
// ...
}
// 枚举所有副作用类型(简化版)
const NoFlags = 0b00000
const Placement = 0b00001
const Update = 0b00010
const Deletion = 0b00100
// ...更多类型三、如何通过 flags 和 subtreeFlags 检测渲染完成?
当 React 处理 Fiber 树时,会递归地收集“副作用标记”:
- 给 Fiber 节点设置
flags
比如一个组件需要更新内容,它的 Fiber 节点会被标记 Update:
javascript
function markUpdate(fiber) {
fiber.flags |= Update // 按位或,设置 Update 标记
}- 给子树设置
subtreeFlags
如果一个父节点的子树中有节点需要插入(Placement),父节点的 subtreeFlags 会被标记:
javascript
function markSubtree(fiber) {
// 递归遍历子树,收集所有副作用
if (fiber.child) {
markSubtree(fiber.child)
fiber.subtreeFlags |= fiber.child.subtreeFlags
}
if (fiber.sibling) {
markSubtree(fiber.sibling)
fiber.subtreeFlags |= fiber.sibling.subtreeFlags
}
// 同时合并自身的 flags 到 subtreeFlags
fiber.subtreeFlags |= fiber.flags
}- 判断是否渲染完成
当一个 Fiber 节点的 flags 和 subtreeFlags 都为 NoFlags 时,说明它自身和子树都没有需要执行的副作用,即渲染完成:
javascript
function isFiberRendered(fiber) {
return fiber.flags === NoFlags && fiber.subtreeFlags === NoFlags
}四、实际流程示例
假设我们有一个 Fiber 树结构:
- 根节点
A,子节点B,B的子节点C。 C需要执行「更新」操作(flags = Update)。
执行 markSubtree(A) 后:
C.flags = UpdateB.subtreeFlags = Update(因为子节点C有副作用)A.subtreeFlags = Update(因为子节点B的子树有副作用)
此时 isFiberRendered(A) 会返回 false(因为 A.subtreeFlags !== NoFlags)。
当 C 的更新操作执行完毕后,会清除 C.flags 并重新收集子树标记:
C.flags = NoFlagsB.subtreeFlags = NoFlags(子树无副作用)A.subtreeFlags = NoFlags(子树无副作用)
此时 isFiberRendered(A) 返回 true,说明整个树渲染完成。
通过这种机制,React 能精准地知道“哪些节点还需要处理副作用”,从而高效地调度渲染流程~
- 双缓存树优化 Diff (Fiber Tree):Fiber 架构中有两棵 Fiber 树——
current fiber tree(当前正在渲染的 Fiber 树)和work in progress fiber tree(正在处理的 Fiber 新树),在新树构建完成后一次性替换旧树(curent fiber)。React 使用这两棵树来保存更新前后的状态,从而更高效地进行比较和更新,保证渲染到屏幕的始终是完整的 UI。
双树的作用
React 采用双 Fiber 树(current 树和 work in progress 树)来保存更新前后状态,主要是为了实现渲染的原子性、可中断性
- 保证渲染的原子性
通过先在 work in progress 树中完成所有更新计算,再一次性替换 current 树,能确保用户看到的始终是完整、一致的 UI,避免“半成品”渲染
- 支持可中断的渲染调度
双树结构让 React 可以:
- 在构建
work in progress树时,若有更高优先级任务(如用户点击、动画),随时中断当前构建过程,优先处理高优先级任务 - 后续恢复时,从 work in progress 树的 “断点” 继续构建,无需从头开始,大幅提升渲染效率。
总结:
work in progress树是 React 在计算更新阶段构建的 “临时树”,它承载了最新的状态、属性和 DOM 变更。React 会在这棵树上执行 Diff 算法、计算副作用(如哪些节点需要插入、更新、删除),并完成所有 “逻辑层面的更新计算”。current树是当前已经渲染到屏幕上的 “真实” 树,它反映了用户当前看到的 UI 状态。只有当 WIP 树完全构建好(所有更新逻辑计算完毕),React 才会一次性替换 current 树,让新的 UI 原子性地渲染到屏幕上。
简单来说,WIP 树是 “草稿纸”(用来计算更新),current 树是 “最终作品”(用来渲染到屏幕)。
两树在 react fiber 更新的工作阶段
- Work In Progress(WIP)树:工作在 协调阶段(Reconciliation),核心是计算更新(Diff 对比、生成新 Fiber 节点、标记副作用)。
- current 树:在协调阶段作为“旧状态参考”,最终在 提交阶段(Commit) 被 WIP 树替换,新 UI 渲染到屏幕。
Fiber 节点
Fiber 节点是虚拟 DOM 节点的增强版数据结构。每个 fiber 节点都包含了当前组件的类型、props 状态和父子关系、更新优先级。
每个组件对应一个 Fiber 节点,构成双向链表树结构,包含以下关键信息:
- 组件类型:函数组件、类组件或原生标签。
- 状态与副作用:Hooks 状态(如
useState)、生命周期标记(如useEffect)。 - 调度信息:任务优先级(
lane模型)、到期时间(expirationTime)。 - 链表指针:
child(子节点)、sibling(兄弟节点)、return(父节点)。
js
// Fiber 节点结构简化示例
const fiberNode = {
tag: FunctionComponent, // 组件类型
stateNode: ComponentFunc, // 组件实例或 DOM 节点
memoizedState: {
/* Hooks 链表 */
},
pendingProps: {
/* 待处理 props */
},
effectTag, // 标记当前节点需要执行的操作(如插入、更新、删除)
lanes: Lanes.HighPriority, // 任务优先级
child: nextFiber, // 子节点
sibling: null, // 兄弟节点
return: parentFiber // 父节点
}React Fiber 更新过程
React Fiber 更新过程核心是 “调度-协调-提交”三个阶段循环,具体步骤如下:
- Scheduler(调度阶段):定优先级、排执行顺序
- 优先级划分:采用 Lane 模型(React 17+),用二进制位标记优先级,从高到低核心分类为:紧急任务(如用户输入、点击)> 普通更新(如数据渲染)> 低优先级任务(如懒加载)。
二进制位标记优先级 -> 优先级划分
js
// 最高优先级:同步任务(如 flushSync、生命周期钩子)
export const SyncLane = 0b0000000000000000000000000000001
// 高优先级:用户交互(如点击、输入、滚动)—— 需立即响应
export const InputContinuousLane = 0b0000000000000000000000000000010
export const InputDiscreteLane = 0b0000000000000000000000000000100
// 中优先级:定时器(setTimeout、setInterval)
export const TimerLane = 0b0000000000000000000000000001000
// 低优先级:网络请求、数据获取(如 Suspense 加载)
export const NetworkLane = 0b0000000000000000000000000010000
// 最低优先级:空闲任务(如日志上报、非紧急更新)
export const IdleLane = 0b1000000000000000000000000000000- 空闲检测:利用浏览器 requestIdleCallback 或自定义 polyfill,仅在主线程空闲时执行低优先级任务,避免阻塞。
Reconciliation(协调阶段):此阶段会进行 Fiber 树的构建(包括新 Fiber 树的创建和与旧 Fiber 树的对比,双树对比,即协调过程),同时标记出需要执行的副作用(如 DOM 操作、生命周期方法调用等)。由于该阶段的操作不直接涉及 DOM 修改,这个阶段是可以被中断的。
Commit 阶段:阶段的主要任务是将 Reconciliation 阶段标记的副作用应用到实际 DOM 上,包括执行 DOM 增删改操作、调用相关生命周期方法等,这个阶段必须一次性完成,不能中断。
Fiber 如何实现可中断更新?
将渲染工作拆分为小单元(Fiber 节点),通过链表结构存储 Fiber 节点,记录遍历进度。每次处理一个 Fiber 后检查剩余时间切片,不足时保存当前进度,将控制权交还浏览器。
通过指针记录当前遍历位置,后续可从该位置恢复
具体实现原理
- Fiber 数据结构:工作单元的拆分
Fiber 本质上是对 React 元素的重新封装,每个 Fiber 节点对应一个组件或 DOM 元素,同时包含以下关键信息:
- child/return/sibling:构建 Fiber 树的链表结构(替代传统栈递归),支持“指针跳转”式遍历,而非一次性递归到底。
- pendingProps/memoizedProps:用于对比前后属性变化。
- effectTag:标记当前节点需要执行的操作(如插入、更新、删除)。
- alternate:指向“双缓存”中的另一棵 Fiber 树(current 树与 workInProgress 树)。
这种链表结构允许 React 随时暂停当前节点的处理,通过指针记录当前遍历位置,后续可从该位置恢复。
- 链表结构:记录遍历进度
Fiber 节点通过 child(子节点)、sibling(兄弟节点)、return(父节点)形成链表结构,可以随时记录 “当前处理到哪个节点”。比如处理完一个 Fiber 节点的子节点后,能通过 sibling 找到下一个兄弟节点继续处理,也能通过 return 回到父节点,从而实现 “中断后能恢复” 的遍历逻辑。
- 时间切片:控制执行时长
React 利用浏览器的 requestIdleCallback 思想(实际使用自定义调度器 scheduler),将每段工作限制在一个“时间切片”(通常约 5ms)内:
- 每次处理一个 Fiber 节点后,检查是否已超过当前时间切片。
- 若超时,将控制权交还给浏览器(处理用户输入、渲染等紧急任务),并在下次空闲时恢复工作。
核心代码逻辑类似:
js
function workLoop(hasTimeRemaining) {
let currentFiber = nextUnitOfWork
while (currentFiber && hasTimeRemaining()) {
// 处理当前 Fiber 节点
currentFiber = performUnitOfWork(currentFiber)
// 检查剩余时间
hasTimeRemaining = checkTimeRemaining()
}
// 保存当前进度,下次恢复
nextUnitOfWork = currentFiber
}- 优先级调度:决定任务中断与恢复
React 为不同任务分配优先级(如用户输入 > 动画 > 普通更新),通过调度器(Scheduler)实现任务的中断与切换:
- 高优先级任务(如点击事件)可以打断正在执行的低优先级任务。
- 被打断的任务不会被丢弃,而是保存在
workInProgress树中,待高优先级任务完成后,低优先级任务可从断点继续执行(或基于最新状态重新开始)。
优先级通过过期时间(expiration time)管理:优先级越高,过期时间越近,越先被执行。
- 双缓存机制:确保状态一致性
React 维护两棵 Fiber 树:
- current 树:对应当前已渲染到 DOM 的状态。
- workInProgress 树:正在构建的新树,所有更新操作在这棵树上进行。
当工作被中断时,workInProgress 树保存当前进度;当整棵树构建完成后,通过切换 current 指针指向新树,实现 DOM 的批量更新,避免中间状态的渲染。
Fiber 中断后恢复,如何知道节点处理没有呢,以及从哪个节点开始恢复呢?
要判断 Fiber 节点是否处理完成,React 是通过 Fiber 节点的 “完成标记” 和遍历流程的状态记录 来实现的,具体逻辑如下:
- 基于「完成标记」的直接判断
在 Fiber 节点的处理过程中,React 会通过 flags 和 subtreeFlags 字段 标记节点是否还有未完成的副作用(如 DOM 操作、状态更新等)。如果一个 Fiber 节点的 flags(自身副作用)和 subtreeFlags(子树副作用)都为 “无标记”(值为NoFlags),则说明该节点及其子树已经处理完成。
code
js
// Fiber 节点结构(简化)
class FiberNode {
constructor() {
this.flags = 0 // 自身副作用标记(如更新、插入)
this.subtreeFlags = 0 // 子树副作用标记
this.child = null // 子节点
this.sibling = null // 兄弟节点
this.return = null // 父节点
this.effectTag = null // 额外的完成标记(可选)
}
}
// 判断节点是否处理完成
function isFiberCompleted(fiber) {
return fiber.flags === 0 && fiber.subtreeFlags === 0
}- 基于「遍历流程」的进度记录
Fiber 的链表结构(child、sibling、return)本身就是 “遍历进度” 的记录器。React 在处理 Fiber 树时,会通过 “当前处理节点的指针” 来跟踪进度,即使中途中断,也能从 “断点” 继续:
- 首先从根节点开始遍历
code
js
let currentFiber = rootFiber // 根节点
while (currentFiber) {
// 处理当前 Fiber 节点(如计算 Diff、标记副作用)
processFiber(currentFiber)
// 优先处理子节点
if (currentFiber.child) {
currentFiber = currentFiber.child
} else {
// 没有子节点,处理兄弟节点
while (currentFiber && !currentFiber.sibling) {
// 若当前节点和兄弟节点都处理完,回到父节点
currentFiber = currentFiber.return
}
if (currentFiber) {
currentFiber = currentFiber.sibling
}
}
}- 中断与恢复的关键 ——“保存当前 Fiber 指针”
如果在遍历过程中需要中断(如时间片用尽),React 会保存当前的 currentFiber 指针。等后续恢复时,直接从这个指针继续遍历即可,无需从头开始。(可以看上面的代码)
问题 5:React concurrency 并发机制
什么是 React 的并发机制?
React 的并发机制(Concurrency)是 React 18 引入的一项重要特性,旨在提升应用的响应性和性能。
允许 React 在渲染过程中根据任务的优先级进行调度和中断,从而确保高优先级的更新能够及时渲染,而不会被低优先级的任务阻塞。
并发机制的工作原理
时间分片(Time Slicing): React 将渲染任务拆分为多个小片段,每个片段在主线程空闲时执行。这使得浏览器可以在渲染过程中处理用户输入和其他高优先级任务,避免长时间的渲染阻塞用户交互。
优先级调度(Priority Scheduling): React 为不同的更新分配不同的优先级。高优先级的更新(如用户输入)会被优先处理,而低优先级的更新(如数据预加载)可以在空闲时处理。
可中断渲染(Interruptible Rendering): 在并发模式下,React 可以中断当前的渲染任务,处理更高优先级的任务,然后再恢复之前的渲染。这确保了应用在长时间渲染过程中仍能保持响应性。
问题 6:React reconciliation 协调的过程
React 的 协调(Reconciliation) 是用于高效更新 UI 的核心算法。当组件状态或属性变化时,React 会通过对比新旧虚拟 DOM(Virtual DOM)树,找出最小化的差异并应用更新。以下是协调过程的详细步骤:
生成虚拟 DOM 树:当组件状态或属性变化时,React 会重新调用组件的
render方法,生成新的虚拟 DOM 树。Diffing 算法(差异对比):React 使用 Diffing 算法 比较新旧两棵虚拟 DOM 树,找出需要更新的部分。
更新真实 DOM:通过 Diffing 算法找出差异后,React 将生成一系列最小化的 DOM 操作指令(例如
updateTextContent、replaceChild)。这些指令会被批量应用到真实 DOM 上,以减少重绘和重排的次数,提高性能。协调的优化策略:
- Key 的作用:为列表元素提供唯一的
key,帮助 React 识别元素的移动、添加或删除,避免不必要的重建。 - 批量更新(Batching):React 会将多个状态更新合并为一次渲染,减少重复计算。
- Fiber 架构(React 16+):将协调过程拆分为可中断的“工作单元”(Fiber 节点),允许高优先级任务(如动画)优先处理。支持异步渲染(Concurrent Mode),避免长时间阻塞主线程。
- Key 的作用:为列表元素提供唯一的
问题 7:JSX 是怎么一步步转化成 Fiber 的?
JSX 其实不是浏览器能识别的语法,本质是一个语法糖,最终被 Babel 编译为React.createElement调用,这个调用返回的是一个ReactElement的普通 js 对象,不涉及真实渲染。真正的渲染是 react 根据它来构建 fiber 树。
构建 fiber 树的过程发生在 render 阶段,react 会遍历每个组件,执行beginWork和completeWork两个流程,边遍历边构建新的 fiber 树,这个过程最大的特点就是:可中断的。也就是说,渲染任务中有优先级更高的任务,例如用户点击了按钮,react 可以暂停当前的渲染,优先处理高优先级任务,就避免了页面“一卡一卡”的问题。
等 fiber 树构建完成后,会进入commit阶段,会把已经标记好变化的节点一次性更新到真实 dom 上,包括插入、更新、删除等操作。
所以整个流程分为:jsx 编译成 ReactElement,react 根据 ReactElement 构建 fiber 树,fiber 架构让渲染变得可中断、可调度,最后进入 commit 阶段统一更新 dom。
- beginWork:“规划阶段”——找要更新的节点、算清楚怎么更(对比新旧)、标记好要做的事(Reconciliation 阶段);
- completeWork:“执行阶段”——按规划操作 DOM(增删改)、收拾收尾(样式/副作用),落地到页面(Reconciliation 阶段)。
问题 8:为什么 Hook 调用顺序必须一致?
因为 React 内部通过一个链表来跟踪组件中所有 Hook 的状态。
React 并不能通过 Hook 的名称或参数来识别不同的 Hook,而是完全依赖于调用顺序来匹配对应的状态。
条件判断会破坏调用顺序,会导致每次渲染时 Hook 的调用顺序不一致,React 就无法正确关联之前存储的状态。
解释
React 依赖于 Hook 的调用顺序来正确关联状态。在每次渲染时,Hook 的调用顺序必须完全相同,如果 Hook 调用顺序改变(比如由于条件语句),React 就无法正确跟踪状态。
因为在 React 中,Hook 调用顺序必须保持一致是由其内部实现机制决定的,主要原因有以下几点:
React 对 Hook 的管理方式: React 内部通过一个链表来跟踪组件中所有 Hook 的状态。每个 Hook 调用(如
useState、useEffect)都会对应链表中的一个节点,记录着该 Hook 的状态信息。依赖调用顺序识别 Hook: React 并不能通过 Hook 的名称或参数来识别不同的 Hook,而是完全依赖于调用顺序来匹配对应的状态。例如:
jsxfunction Component() { const [name, setName] = useState('') // 第1个Hook const [age, setAge] = useState(0) // 第2个Hook useEffect(() => { /* ... */ }, []) // 第3个Hook // ... }React 会默认认为每次渲染时,第 1 个
useState始终对应 "name" 状态,第 2 个useState始终对应 "age" 状态。条件判断会破坏调用顺序: 如果在条件语句(如
if)、循环或嵌套函数中调用 Hook,会导致每次渲染时 Hook 的调用顺序不一致。例如:jsxfunction Component() { if (someCondition) { const [name, setName] = useState('') // 可能不会被调用 } const [age, setAge] = useState(0) // 调用顺序可能变化 // ... }当
someCondition为false时,age状态会被 React 误认为是第一个 Hook,导致状态匹配错误,进而引发难以预测的 Bug。保证状态一致性: 一致的调用顺序确保了 React 能够在多次渲染之间正确关联 Hook 与其对应的状态,这是 React 实现 Hook 机制的基础。
因此,React 官方强制要求:Hook 必须在函数组件的顶层调用,不能在条件、循环或嵌套函数中使用,以保证每次渲染时 Hook 的调用顺序完全一致。
问题 9:Hooks 的链表结构
React 内部通过单向链表结构来管理组件中的 Hooks,这个链表是 Hooks 能够在多次渲染之间保持状态的核心机制。我们可以从以下几个方面理解这个结构:
1. 链表的基本构成
每个 Hook 对应链表中的一个节点,节点中包含以下关键信息:
memoizedState:存储当前 Hook 的状态(如useState的值、useEffect的依赖项等)next:指向链表中的下一个 Hook 节点(形成单向链表)- 其他元数据:如
useEffect的回调函数、清理函数等
链表结构
js
const hook1 = {
memoizedState: null,
next: hook2
}
const hook2 = {
memoizedState: null,
next: null
}fiber 和 hooks 结构
js
// 1. Fiber 节点:memoizedState 存储的是「当前组件的 Hooks 链表头」
const fiber = {
memoizedState: hook1 // Fiber 用 memoizedState 关联 Hooks 链表
// 其他 Fiber 必需属性(省略,如 type、child、sibling 等)
}
// 2. Hook 节点:核心属性 + 链表关联
const hook1 = {
memoizedState: 'hook1的状态值', // 存储当前 Hook 的「缓存状态」(如 useState 的值、useEffect 的依赖/回调)
next: hook2, // 链表下一个 Hook(保证 Hooks 顺序不变)
queue: { pending: null } // 存储 Hook 的「更新队列」(如 useState 的 setX 触发的更新)
}
const hook2 = {
memoizedState: 'hook2的状态值',
next: null, // 链表尾
queue: { pending: null }
}2. 链表的创建与更新过程
当组件首次渲染时:
- React 会初始化一个
workInProgressHook指针,指向当前正在处理的 Hook 节点
在 React 并发模式下,
workInProgressHook指针能帮 React 标记当前处理到哪个 Hook 了,要是遇到高优先级务打断,它能记住位置,等合适了接着处理。
- 每调用一个 Hook(如
useState),React 就会创建一个新的节点,将其添加到链表尾部 - 节点的
memoizedState会存储初始状态,next指针指向下一个新创建的节点
当组件重新渲染时:
- React 会重置
workInProgressHook指针,使其指向链表的头部 - 按相同的顺序再次调用 Hooks 时,指针会依次移动到下一个节点,复用之前存储的
memoizedState
初次渲染时memoizedState存储初始值,重渲染时就按顺序复用这些已有的状态值,保证每次渲染时 Hooks 状态的稳定和正确。
简化的工作流程示例
假设组件中有两个 useState:
jsx
function MyComponent() {
const [count, setCount] = useState(0); // Hook 1
const [name, setName] = useState(''); // Hook 2
return ...
}首次渲染:
- 调用
useState(0)→ 创建节点 1(memoizedState=0,next=null) - 调用
useState('')→ 创建节点 2(memoizedState='',next=null),并将节点 1 的next指向节点 2 - 形成链表:
节点1 → 节点2
- 调用
重新渲染:
- 再次调用第一个
useState→ 指针指向节点 1,读取其memoizedState(0) - 再次调用第二个
useState→ 指针移动到节点 2,读取其memoizedState('') - 保持链表结构不变,状态正确复用
- 再次调用第一个
3. 为什么只能在函数组件中使用?
因为 Hooks 需要绑定当前组件的 Fiber 节点,依赖函数组件的执行上下文,而普通函数无此上下文。
问题 10:React 的 hooks 的原理是怎样的
React Hooks 的核心原理本质上是基于 React 内部维护的“Hook 链表”来实现的,React 会为每个函数组件实例维护一个对应的 Fiber 节点,而 Fiber 节点中又挂载了一个保存 Hook 相关信息的链表结构。
当组件首次渲染时,每调用一次 Hook(比如 useState、useEffect),React 都会创建一个对应的 Hook 对象,这个对象会包含 Hook 的状态值、更新队列、依赖项等关键信息,并将其依次挂载到该组件 Fiber 节点的 Hook 链表上;而当组件触发重新渲染时,React 会按照首次渲染时的 Hook 调用顺序,从头遍历这个链表,依次读取或更新每个 Hook 对象中的数据——这也是为什么 Hook 必须在函数组件的顶层调用、不能放在条件判断或循环中的核心原因,一旦调用顺序被打破,React 就无法正确匹配链表中的 Hook 对象,进而导致状态错乱。
以 useState 为例,其内部会维护一个更新队列,调用 setState 时并不会立即修改状态,而是将更新任务加入队列,React 会触发组件重新渲染,在渲染过程中遍历 Hook 链表找到对应的 useState Hook 对象,执行队列中的更新逻辑并计算出新的状态值,再将新状态更新到 Hook 对象中并返回;而 useEffect 则是在组件渲染完成后,React 会对比当前 Hook 对象中保存的依赖项和上一次的依赖项,若依赖项发生变化,就执行副作用函数,同时清理上一次的副作用(若有),并将新的依赖项更新到 Hook 对象中。
整体而言,React Hooks 就是通过将状态和副作用等逻辑与函数组件的 Fiber 节点绑定,借助固定顺序的 Hook 链表来管理每个 Hook 的生命周期和数据,从而让无状态的函数组件具备了状态管理、生命周期等能力,同时保证了 Hook 调用与状态的一一对应,确保组件状态的正确维护。
模拟结构
js
// 1. 模拟函数组件对应的 Fiber 节点
const Fiber = {
// memoizedState 存储该组件的 Hooks 链表(头节点)
memoizedState: {
// 第一个 Hook(对应 const [count, setCount] = useState(0))
memoizedState: 0, // 存储 useState 的当前状态值(核心!)
queue: { pending: null }, // 存储更新队列(React 内部用)
next: {
// 第二个 Hook(对应 const [name, setName] = useState("React"))
memoizedState: 'React', // 第二个 useState 的当前状态值
queue: { pending: null },
next: null // 链表尾
},
// setState 是关联的更新函数(逻辑绑定在 Hook 上,此处简化表示)
setState: (newState) => {
/* 触发状态更新 */
}
}
}hooks 原理(核心)
1. Hooks 的整体执行入口:renderWithHooks
函数组件在 render 阶段并不是直接执行组件函数,而是统一在:
ts
renderWithHooks(current, workInProgress, Component, props, secondArg)核心职责:
- 判断渲染类型
- 根据
current === null判断是首次渲染(mount)还是更新(update)。
- 根据
- 为当前 Fiber 准备不同的 Hooks Dispatcher
- mount 阶段:
HooksDispatcherOnMount - update 阶段:
HooksDispatcherOnUpdate
- mount 阶段:
- 重置 hooks 指针,保证 hooks 按“声明顺序”依次执行。
2. Hooks 的存储结构:单向链表 + 循环链表
(1) Hooks 本体:单向链表
- 挂载位置:每个函数组件的 hooks 都挂在
FiberNode.memorizedState上。 - 链表入口:
memorizedState指向单向链表的第一个 hook。 - 每个 hook 结构类似:
js
hook = {
memoizedState,
baseState,
queue,
next
}- hooks 通过
next串成 单向链表 - 最后一个 hook 的
next === null
这是为什么 Hooks 必须按固定顺序调用 的根本原因。
(2) State Hook 的更新队列:循环链表
以 useState 为例:
- 每个 state hook 都有一个
queue queue.pending指向一个 环形链表- 每个 update:
javascript
update = {
action,
next
}- 多次
setState会不断往这个环上追加update - render 阶段会 从
baseState出发,按顺序执行update
(3) Effect: 单独的 effectList 循环链表
useEffect/useLayoutEffect不在 hook 链表**里执行副作用- 它们会生成 effect 对象
- 通过
fiber.updateQueue.lastEffect形成effectList循环链表 - commit 阶段统一遍历执行
3. Hooks 的数据到底存在哪?
结论一句话:hooks 的所有状态数据,最终都存储在 FiberNode.memoizedState 这条 hooks 链表上。
4. Hooks 链表是何时构建的?
在第一次调用 useXxx API 时构建。核心方法:
mount 阶段流程:
- 每次调用
useXxx - 内部都会调用
mountWorkInProgressHook - 创建一个新的 hook 对象
- 如果是第一个 hook:
- 挂到
fiber.memoizedState
- 挂到
- 否则:
- 接到上一个 hook 的
next
- 接到上一个 hook 的
js
fiber.memoizedState -> hook1 -> hook2update 阶段:
- 使用
updateWorkInProgressHook - 从
current.memoizedState中 按顺序复用 hook - 同时构建新的 WIP hooks 链表
5. 为什么 Hooks 不能写在条件语句中?
简单来说:Hooks 的匹配完全依赖“调用顺序”,而不是名字或 key。
本质原因:
- current Fiber 上:
memoizedState保存的是上一次 render 的 hooks 链表
- 本次 render:
- 通过指针顺序读取 hooks
- 一旦条件改变:
- hooks 数量或顺序发生变化
- current 和 workInProgress 的 hooks 无法一一对应
- 读取到错误的 state,直接触发异常
这是一个 链表错位问题,不是语法限制。
问题 11:Hooks 索引
Hooks 索引是 React 内部用于追踪组件中 Hooks 调用顺序的「隐形指针」,也就是 workInProgressHook,确保每次渲染时,useState、useEffect 等 Hooks 的执行顺序与初始化时完全一致,从而正确关联对应的状态和副作用。
核心逻辑可简化为 3 点:
- 组件首次渲染时,Hooks 索引从 0 开始,每调用一个 Hook,索引就 +1,并将 Hook 的状态、副作用等信息存入「Hook 链表」。
- 组件更新渲染时,Hooks 索引重置为 0,再次按顺序调用 Hooks,通过索引从链表中读取对应的数据,保证状态不错乱。
- 若在条件判断(如
if)、循环(如for)中调用 Hooks,会破坏索引顺序,导致 React 无法匹配正确的 Hook 数据,引发 bugs。
实际不是索引,是 hooks 指针
问题 12:useEffect 的底层是如何实现的
需要结合 React 的渲染流程、Fiber 架构和 Hook 链表机制
useEffect 底层依托 React 的 Fiber 节点和 Hook 链表实现。
组件首次渲染时,useEffect 会被封装成包含副作用函数、依赖项的 Hook 对象,挂载到当前组件的 Fiber 节点 Hook 链表中。
它的执行时机在组件 DOM 更新完成后,也就是在 commit 阶段的末尾统一执行,确保副作用不会影响渲染。
当组件重新渲染时,React 会对 Hook 对象中缓存的上一次依赖项和当前依赖项做浅比较,若依赖发生变化,会先执行上一次副作用返回的清理函数,再执行新的副作用函数,并更新 Hook 对象中的依赖和清理函数;
当组件卸载时,会遍历 Hook 链表执行所有 useEffect 的清理函数,以此保证副作用的可控执行和资源释放。
问题 13:useState 实现原理
useState 底层依托 React 的 Fiber 节点和 Hook 链表、更新队列(update queue)实现。
组件首次渲染(mount)时,useState 会被封装成一个包含初始状态、更新队列的 Hook 对象,挂载到当前组件的 Fiber 节点的 Hook 链表中。此时会初始化 memoizedState 为传入的初始值,并创建一个用于存储更新操作的单向链表结构的 update queue。
当调用 setState 时,会创建一个包含新状态或状态更新函数的 update 对象,并将其推入 update queue 的 pending 链表中,随后触发 React 的调度机制(Scheduling),标记当前组件需要重新渲染。
组件进入渲染(render)阶段时,会遍历当前 Hook 的 update queue,依次执行所有 update 对象中的更新逻辑,计算出最新的状态值,并将这个新值更新到 Hook 对象的 memoizedState 中,最终作为本次渲染的状态返回。
在后续的重新渲染中,React 会通过 Hook 链表的顺序来匹配对应的 useState Hook,确保状态和更新队列的正确关联,不会因为 Hook 的调用顺序变化而导致状态错乱。