前言
React 开启 Concurrent 后,可以做到可中断的渲染和优先级更新。但时常有人质疑,在大部分场景下是否现有的防抖节流就能满足需求,客户端性能更好则防抖时间更短这种特性是否真的有必要,高大上的概念是否真的为 React 带来与众不同的优越性?本文希望通过一个例子来说明。
在线演示
点击 getData ,在下方渲染40000条数据。
分别在入口开启和不开启 Concurrent , 在新 tab 开启演示页面,使用浏览器 Perfomance 录制性能。 注意测试 concurrent时要先点击几次 test JS block 再点 getData。
渲染流程
众所周知,一次 setState 到页面变化 React 大致经历两个过程:render 和 commit。
render:计算出新 Fiber 树
commit:将新 fiber 树渲染成真实 DOM
观察录制情况:
开启 Concurrent
可以看到,左边比较细碎的即 Concurrent 的 render ,右边commitRoot 即commit。
可中断的渲染指的是左边细碎的 render部分。
在这个典型场景下,总渲染时间 8s 左右,Concurrent 指的是前 1-2s 的范围。
关闭 Concurrent
可以看到,commitRoot 前的 render 不再细碎。
优越性
Concurrent 与众不同的优越性在此体现:
开启 Concurrent
点击 getData 后,2S 左右内不断点击 test JS block 仍然可以更新数字,即render阶段,Concurrent可以不阻塞页面。 之后commit 阶段的 6s 界面阻塞。
关闭 Concurrent
可以发现,点击 getData 后界面直接阻塞,点击 test JS block 无效。
现象和上文分析一致,由此演示了 Concurrent 的优越性。
思考
众所周知,React 的性能并不好,只是刚好够用。React 是 comparison(比较式响应),相比于 Vue,Angular 使用的 subscription(订阅式响应)为主的 reactivity 响应有天然的弱势。任何时候更新中的 comparison overhead 是无法忽略的性能损耗。
可以发现 Concurrent 模式只是把 comparison 的计算过程优化成不阻塞了(严格上来说是不阻塞更高优先级更新如demo中 test js block 按钮,你甚至要像演示代码这样手动控制更新的优先级!),根本上解决的还是 React 自身的问题。如 github.com/vuejs/rfcs/… ,Concurrent 解决的问题在订阅式响应的场景下并不存在。
相信总有一天 React 也会拥抱 reactivity 响应,如同现在 React 使用 Mobx、Recoil、zustand、jotai 这样大势所趋。
React concurrent 源码
典型 Fiber 节点字段值,如 Input
const fiberNode = {
// 1. 节点类型相关(识别组件类型与关联实例)
tag: 5, // 5 对应 HostComponent(原生 DOM 组件,如 input)
key: null, // 无 key
elementType: 'input', // JSX 元素类型
type: 'input', // 实际类型(原生 input 标签)
stateNode: document.querySelector('input#username'), // 关联的真实 DOM 节点
// 2. 结构相关(构建 Fiber 树结构,支持中断后恢复遍历)
return: { /* 父 Fiber 节点(如 div 容器)*/ }, // 指向父节点
child: null, // 无子节点(input 是叶子节点)
sibling: { /* 下一个兄弟 Fiber 节点(如按钮组件)*/ }, // 指向兄弟节点
index: 0, // 在父节点子列表中的索引(第一个子节点)
// 3. 状态与更新相关(管理组件的 props 和状态)
pendingProps: { value: 'newInputValue', onChange: () => {} }, // 待应用的新 props(用户输入后)
memoizedProps: { value: 'oldInputValue', onChange: () => {} }, // 已缓存的旧 props
memoizedState: {
// 第一个 useState 钩子
memoizedState: 'initialValue', // useState 的当前状态值
baseState: 'initialValue',
queue: { pending: null }, // 该钩子的更新队列
next: {
// 第二个 useEffect 钩子
tag: 3, // Passive 标记(useEffect)
create: () => { console.log('effect'); return () => {}; }, // 回调函数
destroy: null, // 清理函数(尚未执行)
deps: [/* 依赖数组 */],
next: {
// 第三个 useRef 钩子
memoizedState: { current: null }, // useRef 的当前值,存储在 current 属性中
next: null // 无更多钩子。
// 更新ref 是赋值操作不是 setValue, react 不知道这个更新
// 同时在 Reconcile 里, 只有有 queue 的钩子(如 useState)才可能触发更新
}
}
},
updateQueue: { // 待执行的更新队列(用户输入触发的更新)
shared: {
pending: {
lane: 0b00000001, // SyncLane(同步优先级)
action: (prev) => ({ ...prev, value: 'newInputValue' }),
next: null
}
}
},
// 4. 优先级与调度相关(标记任务优先级,用于筛选执行)
lanes: 0b00000001, // 当前节点的优先级(SyncLane,用户输入是最高优先级)
childLanes: 0b00000000, // 子树无其他优先级任务
// 5. 副作用相关(标记需要执行的 DOM 操作)
effectTag: 0b00000100, // Update 标记(需要更新 DOM 属性)
nextEffect: { /* 下一个有副作用的 Fiber 节点(如父容器的样式更新)*/ }, // 副作用链表指针
firstEffect: null, // 子树中第一个副作用节点(自身是叶子节点,故为 null)
lastEffect: null, // 子树中最后一个副作用节点(同上)
// 6. 双缓存相关(关联另一棵树的对应节点)
alternate: { /* current 树中的 input Fiber 节点(旧版本)*/ } // 指向 current 树的对应节点
};
单个Fiber 节点本身的数据结构实际上包含了整个 React runtime 的所有复杂度, runtime 里所有复杂度都是这个数据结构的派生。
Fiber 节点的核心价值在于其混合数据结构(树形关系 + 链表指针)。每次 React 更新时,遍历从根节点 Fiber 开始,通过 child(子节点)、sibling(兄弟节点)、return(父节点)三个指针实现树的遍历。
单次更新 schedule + reconciliation + commit
某个节点触发更新时(如 setState),调用scheduleUpdateOnFiber,仅会向上遍历至根节点,最终标记 rootFiber.pendingLanes,为后续调度器筛选最高优先级任务提供依据,不会遍历整个 Fiber 树的所有节点。对应 Fiber 节点会被标记 lanes(优先级)。
然后调用 ensureRootIsScheduled 函数,将根节点对应的更新任务注册到 Scheduler 中。此时 Scheduler 仅知道 “有一个根节点需要处理更新”,并根据其 pendingLanes 中的最高优先级安排执行时机(而非已经遍历了全树的所有节点)。 ensureRootIsScheduled 会从根节点 pendingLanes 中,通过 getHighestPriorityLane 筛选出当前最高优的 Lane(如 SyncLane),将其作为初始 renderLanes。
以上,单次更新的优先级队列(小根堆)构建完成,且此时只遍历了局部,不用遍历整个 fiber 树。单次更新 scchedule 只执行一次。
第一次全树的遍历(即逐个处理 Fiber 节点)发生在后续的 reconciliation 阶段(由 performUnitOfWork 等函数驱动)
reconciliation 消费这个优先级任务队列(类似小根堆 logN 复杂度 ,头 Fiber node 永远是优先级最高的。然后不断从任务队列取头。)
每次从小根堆取出最高优任务后,renderLanes (值为单个 Lane值)会被设置为该任务的优先级(如 SyncLane 或 UserBlockingLane),随后 React 会以这个 renderLanes 为过滤器,执行一次完整的 “局部调和”(只处理该优先级相关的 Fiber 节点)。
当本次调和完成(即一个批次的优先级的节点都被处理,且没有更高优任务插入打断)即小根堆取出的头的 Lane 值变化时, 即 RenderLanes 将要切换时(如从 SyncLane 切换到 UserBlockingLane),就会立即进入 commit 阶段,将本次调和产生的 effectList(副作用链表)对应的 DOM 操作执行完毕。
已处理的高优先级节点 lanes 会被清理(标记为 NoLane),调和时通过 shouldProcessUnitOfWork 检查,直接跳过其遍历(包括子节点),仅处理低优相关节点。
会不断从当前最高优先级任务中取出下一个工作单元(nextUnitOfWork,单个 Fiber 节点)执行,并通过 shouldYield() 检查是否超过时间切片(通常为 5ms)。若需中断,低优先级任务会被暂停,高优先级任务则抢占执行权,完成自身后续的协调(reconciliation)后进入提交(commit)阶段。
在 Reconciliation 并发调和阶段:更新各种 Fiber 标记字段
- 构建 workInProgress 树(改树结构): 通过
reconcileChildren对比current树与新状态(如 props 变化),为新增节点创建新 Fiber 实例、复用可复用节点、删除废弃节点,直接调整 Fiber 树的child/sibling/return指针关系,形成与新状态匹配的workInProgress树结构。 - 标记副作用(修改属性值): 对需要变更的节点,通过
beginWork标记effectTag(如Placement插入、Update更新、Deletion删除),同时通过completeUnitOfWork将这些节点串联成effectList副作用链表,明确后续 DOM 操作的具体对象和类型。 - 优先级筛选:基于节点的
lanes优先级标记,在遍历过程中通过shouldProcessUnitOfWork等逻辑跳过低优先级节点,仅处理当前最高优先级任务涉及的节点,实现 “高优任务抢占低优任务” 的并发调度能力。高优 Lane node 被renderLanes筛选出,并直接执行 Partial reconcile 局部调和 +commit 直接更新。 然后renderLanes更新成普通的,继续 Partial reconcile 局部调和直到走完,仍然会被插入的高优打断。
在 Commit 提交阶段:只通过遍历 fiber 上的 effectTag,nextEffect 字段指针的副作用链表,只对有标记的节点执行真实 DOM 操作,无变更的节点完全不参与渲染。此阶段执行 DOM 增删改操作,且操作过程不可中断,确保 DOM 一致性。
- commit 阶段的遍历起点总是根节点的
firstEffect:completeUnitOfWork会将所有带effectTag的节点串联成链表,根节点的firstEffect指向链表头,nextEffect指向后续节点。
- 遍历范围仅限有副作用的节点:无论高优还是低优 commit,均从根节点
firstEffect开始,依次遍历nextEffect链表,仅对带effectTag的节点执行 DOM 操作。未标记节点不会出现在链表中,完全不参与遍历。 - 高优与低优 commit 的链表隔离:高优调和仅收集高优相关节点的
effectTag,形成独立副作用链表;低优调和收集低优节点的effectTag,两者互不干扰,commit 时仅处理当前优先级对应的链表。
因此,Fiber 的数据结构是 React runtime 的核心:源码中 performUnitOfWork、beginWork、commitMutationEffects 等核心函数均以 “单个 Fiber 节点” 为入参,通过节点的 child/sibling/return 指针实现局部遍历,间接操作整棵树;节点的 lanes(优先级标记)、effectTag(副作用标记)等字段则承担 “隐式通信” 角色,传递任务状态和操作指令。
当 shouldYield() 触发中断时,只需保存当前 nextUnitOfWork(下一个待处理的 Fiber 节点)即可暂停任务;恢复时从该节点继续遍历,无需保存整棵树的状态。
这种 “单个节点 + 指针遍历” 的设计,既支持了任务的中断与恢复(仅需追踪当前节点),又减少了内存占用(无需在函数上下文中维护整棵树的引用),是 React 实现非阻塞渲染的核心基础。
优先级更新,打断
单次更新触发后,调度阶段(schedule)会收集所有任务队列并并确定优先级队列,随后通过动态切换 renderLanes 分阶段处理:
- 优先以高优
renderLanes筛选节点,执行局部调和(partial reconciliation),仅处理高优相关节点,完成后立即 commit 对应的 DOM 变更; - 随后切换为低优
renderLanes,从上次中断点头的节点继续续执行局部调和,处理低优相关节点,完成后再 commit 其 DOM 变更。
整个过程中,若有更高优先级任务插入,无论论当前处于处理高优还是低优阶段,都会立即中断当前调和,优先处理新的高优任务(重复上述 “局部调和 + commit” 流程),待其完成后再恢复复之前的低优调和。
核心调用链路
┌─────────────────────────────────────────────────────────────────────┐
│ 触发更新(用户操作/ setState/ useTransition) │
└───────────────────────────────┬─────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ 1. 优先级分配阶段 │
│ 函数:scheduleUpdateOnFiber(fiber, lane) │
│ 作用:为更新任务分配 Lane(如 SyncLane),标记 Fiber 节点优先级,
│ 向上遍历至根节点(`rootFiber`),标记根节点的 `pendingLanes`,
确保根节点知道有更新需要处理。
│ 关联:Lane 模型、Fiber 数据结构 │
└───────────────────────────────┬─────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ 2. 调度器入队阶段 │
│ 函数:ensureRootIsScheduled(root) │
│ 作用:获取根节点最高优先级 Lane,创建调和任务并入 LanePriorityQueue │
│ 关联:Scheduler、LanePriorityQueue │
└───────────────────────────────┬─────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ 3. 工作循环启动阶段 │
│ 函数:requestHostCallback(workLoop) │
│ 作用:通过 requestIdleCallback/setTimeout 启动工作循环,绑定时间切片 │
│ 关联:Scheduler、时间切片(shouldYield) │
└───────────────────────────────┬─────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ 4. Reconciliation 并发调和阶段(可中断) │
│ 函数链路: │
│ workLoopConcurrent() → performUnitOfWork(unitOfWork) │
│ → beginWork(current, workInProgress, renderLanes) │
│ → reconcileChildren(workInProgress, current.child) │
│ → completeUnitOfWork(completedWork) │
│ 作用:构建 workInProgress 树,筛选优先级节点,收集副作用 │
│ 关联:Fiber 双缓存、Reconciliation 模块、Lane 筛选 │
└───────────────────────────────┬─────────────────────────────────────┘
┌───────────────────────┴───────────────────────┐
↓ ↓
┌─────────────────────────────┐ ┌─────────────────────┐
│ 时间切片用尽(低优先级任务) │ │ 调和完成 │
│ 函数:shouldYield() │ │ 函数:commitRoot() │
│ 作用: │ │ 作用:进入提交阶段 │
│ 1. 保存当前进度(nextUnitOfWork) │ │
│ 2. 任务重新入队(不提交,无DOM操作) │ │
│ 3. 让出主线程,等待下次调度 │ │
└───────────────┬─────────────┘ └─────────┬───────────┘
│ │
│ ┌───────────────────────────┘
│ │
↓ ↓
┌─────────────────────┐ ┌─────────────────────────────────────────────┐
│ 高优先级任务插入 │ │ 5. commit Root提交阶段(不可中断) │
│ 函数:interruptLowPriorityWork() │
│ 作用: │ │ 函数链路: │
│ 1. 中断低优先级任务 │ │ commitRoot(root) → commitBeforeMutationEffects() │
│ 2. 优先调度高优先级调和任务 │ → commitMutationEffects()(DOM 操作) │
└───────────────┬─────┘ │ → root.current = finishedWork(双缓存切换) │
│ │ → commitLayoutEffects()(useLayoutEffect) │
│ │ → schedulePassiveEffects()(useEffect) │
│ │ 作用:执行 DOM 操作和副作用,更新视图 │
│ └───────────────────┬─────────────────────────┘
│ │
└───────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────┐
│ 6. 清理与重试阶段 │
│ 函数:markRootFinished(root)、upgradeLaneIfNeeded() │
│ 作用: │
│ 1. 清理高优先级任务的 Lane │
│ 2. 恢复低优先级任务调和(从 nextUnitOfWork 继续) │
│ 3. 低优先级任务若被多次打断,升级 Lane 避免饿死 │
│ 关联:Lane 模型、防饿死机制 │
└─────────────────────────────────────────────────────────────────────┘
双缓存 Fiber 树
// 根节点结构
const root = {
current: null, // 当前已渲染到 DOM 的 Fiber 树
containerInfo: document.getElementById('root'), // 挂载容器
};
// 1. 初始化 current 树(首次渲染)
function mountRoot() {
const initialFiber = createFiber(ReactElement); // 从根组件创建 Fiber
root.current = initialFiber;
scheduleUpdateOnFiber(initialFiber); // 触发更新
}
// 2. 构建 workInProgress 树(更新时)
function performUnitOfWork(currentFiber) {
// 根据 current 树克隆出 workInProgress 树(复用未变化节点)
const workInProgress = createWorkInProgress(currentFiber);
// 处理当前节点(计算更新、创建子节点)
reconcileChildren(workInProgress, currentFiber);
// 返回下一个工作单元
return getNextUnitOfWork(workInProgress);
}
// 3. 提交阶段:切换双缓存树
function commitRoot(root) {
const finishedWork = root.finishedWork; // 已完成的 workInProgress 树
// 执行 DOM 操作(插入/更新/删除)
commitMutationEffects(finishedWork);
// 切换 current 树为新构建的树
root.current = finishedWork;
}
current树:与真实 DOM 同步,作为 “旧版本” 参考。workInProgress树:在内存中构建的 “新版本”,修改时不影响真实 DOM。- 提交阶段:
workInProgress树构建完成后,通过root.current = workInProgress原子切换,避免 DOM 抖动。 scheduleUpdateOnFiber开始 child silbling return(parent) 遍历,关联到下文 Lane 和 Scheduler相关逻辑
Reconciliation 阶段,可中断,对应 reconciliation 源码模块
核心函数:performUnitOfWork、reconcileChildren
// 工作循环(可中断)
function workLoopSync() {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
// 可在上面 Performance Record 中找到
function workLoopConcurrent() {
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
// 处理单个 Fiber 节点
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// 1. 执行当前节点的调和逻辑(核心)
const current = unitOfWork.alternate; // current 树中的对应节点
const next = beginWork(current, unitOfWork, renderLanes); // 计算更新,生成子节点
// 2. 保存当前节点的最终 props
unitOfWork.memoizedProps = unitOfWork.pendingProps;
// 3. 确定下一个工作单元(深度优先遍历)
if (next !== null) {
return next; // 优先处理子节点
}
// 无子节点则回溯处理兄弟节点/父节点
let completedWork = unitOfWork;
while (completedWork !== null) {
// 2. 完成当前节点处理(收集副作用)
completeUnitOfWork(completedWork);
// 寻找下一个兄弟节点
if (completedWork.sibling !== null) {
return completedWork.sibling;
}
// 无兄弟节点则返回父节点
completedWork = completedWork.return;
}
return null;
}
// 生成子 Fiber 节点(Diff 算法核心)
function reconcileChildren(current: Fiber | null, workInProgress: Fiber, nextChildren: any) {
if (current === null) {
// 首次渲染:直接创建子 Fiber 树
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren);
} else {
// 更新时:复用现有节点(Diff 对比)
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren);
}
}
Commit阶段,不可中断,对应 commit 源码模块, useEffect useLayoutEffect
核心函数:commitRoot、commitMutationEffects、commitLayoutEffects
function commitRoot(root: FiberRoot) {
const finishedWork = root.finishedWork; // 已完成的 workInProgress 树
// 1. 前置处理(如重置优先级)
root.finishedWork = null;
// 2. 执行 DOM 操作前的副作用(如 getSnapshotBeforeUpdate)
commitBeforeMutationEffects(finishedWork);
// 3. 执行 DOM 变更(核心阶段,不可中断)
commitMutationEffects(finishedWork, root);
// 4. 切换双缓存树(current → workInProgress)
root.current = finishedWork;
// 5. 执行 DOM 操作后的副作用(含 useLayoutEffect 回调)
commitLayoutEffects(finishedWork, root);
// 6. 调度 useEffect 回调(异步执行,不阻塞浏览器渲染)
schedulePassiveEffects();
}
// 3. DOM 变更阶段(删除/插入/更新 DOM)
function commitMutationEffects(finishedWork: Fiber, root: FiberRoot) {
let nextEffect = finishedWork.firstEffect;
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 处理删除节点
if (effectTag & Deletion) {
commitDeletion(nextEffect, root);
}
// 处理插入/更新节点(省略细节)
nextEffect = nextEffect.nextEffect;
}
}
// 5. 布局阶段(执行 useLayoutEffect 回调)
function commitLayoutEffects(finishedWork: Fiber, root: FiberRoot) {
let nextEffect = finishedWork.firstEffect;
while (nextEffect !== null) {
// 执行类组件生命周期(如 componentDidMount/update)
if (nextEffect.tag === ClassComponent) {
const instance = nextEffect.stateNode;
if (instance.componentDidMount) {
instance.componentDidMount();
}
}
// 执行 useLayoutEffect 回调(同步执行,在 DOM 更新后立即触发)
if (effectTag & Layout) {
commitHookEffectListMount(Layout, nextEffect, root);
}
nextEffect = nextEffect.nextEffect;
}
}
// 6. 调度 useEffect 回调(异步执行)
function schedulePassiveEffects() {
// 用微任务或宏任务异步执行,避免阻塞布局
queueMicrotask(() => {
commitPassiveEffects();
});
}
function commitPassiveEffects() {
// 执行 useEffect 回调(在浏览器渲染后触发)
commitHookEffectListMount(Passive, root.current);
}
关键时机:
useLayoutEffect:在commitLayoutEffects中同步执行(DOM 更新后,浏览器渲染前)。useEffect:在schedulePassiveEffects中异步调度,浏览器渲染后执行。
Lane 模型与带 Lane 的优先级队列
Lane 定义
// react-reconciler/src/Lane.js 核心定义
export const NoLanes = 0b00000000;
export const NoLane = 0b00000000;
// 优先级通道(二进制位表示,低位值越小优先级越高)
export const SyncLane = 0b00000001; // 同步优先级(最高)
export const UserBlockingLane = 0b00000010; // 用户阻塞级(动画、输入)
export const NormalLane = 0b00000100; // 普通优先级(网络请求)
export const LowLane = 0b00001000; // 低优先级
export const IdleLane = 0b00010000; // 空闲优先级(最低)
// Lane 工具函数(源码核心逻辑)
export const LaneBitmask = {
// 检查 lanes 中是否包含目标 lane
includes: (lanes, lane) => (lanes & lane) !== NoLanes,
// 合并多个 lane
merge: (lanesA, lanesB) => lanesA | lanesB,
// 从 lanes 中移除目标 lane
exclude: (lanes, lane) => lanes & ~lane,
// 获取最高优先级 lane(最小的二进制位)
getHighestPriorityLane: (lanes) => {
// 位运算技巧:保留最低位的 1(即最高优先级)
return lanes & -lanes;
},
};
// Lane 转调度器优先级(桥接 reconciler 与 scheduler)
export function getSchedulerPriorityForLane(lane) {
if (LaneBitmask.includes(lane, SyncLane)) {
return 1; // ImmediatePriority(立即执行)
} else if (LaneBitmask.includes(lane, UserBlockingLane)) {
return 2; // UserBlockingPriority
} else if (LaneBitmask.includes(lane, NormalLane)) {
return 3; // NormalPriority
} else if (LaneBitmask.includes(lane, LowLane)) {
return 4; // LowPriority
} else if (LaneBitmask.includes(lane, IdleLane)) {
return 5; // IdlePriority
}
return 3; // 默认普通优先级
}
任务队列实现(类似最小堆,头部元素为最高优先级)
类似最小堆算法:构建和存取始终根节点最小即优先级最高,不断取出根节点执行,操作复杂度 logN,构建完全二叉树(每个非叶子子节点左小右大):
-
push 新节点时:LogN 找到插入位置
-
pop 取出根节点时:把最后一个叶子节点挪到根节点位置,重新移动, LogN 构建完全二叉树
/**
* 任务结构(对应源码 Task 类型)
* @param {number} id - 任务唯一标识
* @param {Function} callback - 任务回调(如 performUnitOfWork)
* @param {number} lane - 任务对应的 Lane(二进制位)
* @param {number} expirationTime - 过期时间(毫秒级时间戳)
*/
class Task {
constructor(id, callback, lane, expirationTime) {
this.id = id;
this.callback = callback;
this.lane = lane; // 保留原始 Lane(二进制位)
this.expirationTime = expirationTime;
// 调度器优先级(由 Lane 转换,用于排序)
this.priorityLevel = getSchedulerPriorityForLane(lane);
}
}
/**
* 优先级队列(对应源码 SchedulerPriorityQueue)
* 核心:按 Lane 优先级排序,确保最高优先级任务优先执行
*/
export class LanePriorityQueue {
constructor() {
this._tasks = []; // 任务数组(按优先级排序)
this._nextTaskId = 1; // 自增任务 ID
}
/**
* 添加任务到队列
* @param {Function} callback - 任务执行函数
* @param {number} lane - 任务的 Lane 优先级(二进制位)
* @returns {number} 任务 ID(用于取消任务)
*/
enqueueTask(callback, lane) {
const expirationTime = this._computeExpirationTime(lane);
const task = new Task(
this._nextTaskId++,
callback,
lane,
expirationTime
);
// 插入任务并保持队列有序
this._tasks.push(task);
this._sortTasks();
return task.id;
}
/**
* 取出最高优先级任务
* @returns {Task|null} 最高优先级任务(无任务则返回 null)
*/
dequeueTask() {
if (this._tasks.length === 0) return null;
return this._tasks.shift(); // 队列已排序,头部即最高优先级
}
/**
* 取消指定任务
* @param {number} taskId - 任务 ID
*/
cancelTask(taskId) {
this._tasks = this._tasks.filter(task => task.id !== taskId);
}
/**
* 查看最高优先级任务(不取出)
* @returns {Task|null}
*/
peek() {
return this._tasks[0] || null;
}
/**
* 按优先级排序任务(核心逻辑)
* 1. 先按 priorityLevel 排序(数值越小优先级越高)
* 2. 优先级相同则按 expirationTime 排序(早过期的先执行)
*/
_sortTasks() {
this._tasks.sort((a, b) => {
// 优先按调度器优先级排序
if (a.priorityLevel !== b.priorityLevel) {
return a.priorityLevel - b.priorityLevel;
}
// 优先级相同则按过期时间排序
return a.expirationTime - b.expirationTime;
});
}
/**
* 计算任务过期时间(基于 Lane 优先级)
* @param {number} lane - 二进制 Lane
* @returns {number} 过期时间戳
*/
_computeExpirationTime(lane) {
const now = performance.now();
if (LaneBitmask.includes(lane, SyncLane)) {
return now; // 同步任务立即过期
} else if (LaneBitmask.includes(lane, UserBlockingLane)) {
return now + 250; // 250ms 内过期(用户交互相关)
} else if (LaneBitmask.includes(lane, NormalLane)) {
return now + 5000; // 5s 内过期(普通更新)
} else if (LaneBitmask.includes(lane, LowLane)) {
return now + 30000; // 30s 内过期(低优先级)
} else {
return Infinity; // 空闲任务永不主动过期
}
}
}
- Lane 表示:完全使用二进制位定义(
0bxxxx),与源码字面量一致。 - 位运算逻辑:通过
LaneBitmask工具类实现includes/merge/exclude等核心操作,与源码位运算逻辑完全同步。 - 任务排序:先按
priorityLevel(由 Lane 转换)排序,同优先级按expirationTime排序,与 React 调度器的任务优先级比较逻辑一致。 - 过期时间计算:不同 Lane 对应不同的过期时间阈值(如同步任务立即过期、用户阻塞级 250ms 过期),贴合源码中避免任务饿死的设计。
Reconciliation/Commit 阶段与 Lane 模型的调用关系
-
当触发更新(如
setState、useState、commitRoot)时,通过scheduleUpdateOnFiber为更新任务分配 Lane,决定任务优先级:// 为 Fiber 节点分配 Lane 并触发调度 function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) { // 1. 标记 Fiber 节点及其祖先的 lanes(传播优先级) const root = markUpdateLaneFromFiberToRoot(fiber, lane); if (!root) return; // 2. 确保根节点被调度(关联调度器) ensureRootIsScheduled(root); }- 关键作用:将 Lane 与 Fiber 节点绑定,使调和阶段能识别 “高优先级节点” 优先处理。
-
调和阶段的 Lane 筛选
beginWork函数在处理 Fiber 节点时,通过renderLanes(当前调度的优先级集合)筛选节点,仅处理匹配优先级的更新:function beginWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) { // 检查当前节点的 lanes 是否与 renderLanes 匹配(优先级过滤) if (!isSubsetOfLanes(workInProgress.lanes, renderLanes)) { // 不匹配则跳过当前节点,直接处理兄弟节点 return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } // 匹配则执行正常调和逻辑(计算更新、生成子节点) switch (workInProgress.tag) { case FunctionComponent: return updateFunctionComponent(current, workInProgress, renderLanes); case HostComponent: return updateHostComponent(current, workInProgress); // 其他组件类型处理... } }- 关键作用:确保高优先级 Lane 对应的 Fiber 节点优先调和,低优先级节点暂不处理。
-
提交阶段的 Lane 清理提交完成后,通过
markRootFinished清理根节点的 Lane,释放优先级资源:function commitRoot(root: FiberRoot) { const finishedWork = root.finishedWork; const finishedLanes = root.finishedLanes; // 本次提交的 Lane 集合 // 执行提交逻辑(DOM 操作、副作用)... // 清理根节点的已完成 Lane markRootFinished(root, finishedLanes); }
关键函数总结(串联三大阶段)
| 阶段 | 关键函数 | 作用 |
|---|---|---|
| 优先级分配 | scheduleUpdateOnFiber | 为更新任务分配 Lane,触发调度 |
| 调和过滤 | beginWork | 根据 renderLanes 筛选需处理的 Fiber 节点 |
| 调度触发 | ensureRootIsScheduled | 关联调度器,将带 Lane 的任务入队 |
| 提交清理 | markRootFinished | 清理已完成的 Lane,释放优先级 |
二、Scheduler(调度器)实现(关联 Lane 优先级队列),requestIdleCallback 时间切片
// 调度器核心状态
const scheduler = {
lanePriorityQueue: new LanePriorityQueue(), // 带 Lane 的优先级队列
isRunning: false, // 是否正在执行工作循环
currentTask: null, // 当前执行的任务
};
/**
* 1. 调度根节点更新(调和阶段触发)
* @param {FiberRoot} root - 根节点
*/
function ensureRootIsScheduled(root: FiberRoot) {
// 获取根节点的最高优先级 Lane(如用户输入的 SyncLane)
const nextLanes = getHighestPriorityLane(root.pendingLanes);
if (nextLanes === NoLanes) return; // 无待处理任务
// 创建调和任务(回调为 performConcurrentWorkOnRoot)
const callback = () => performConcurrentWorkOnRoot(root, nextLanes);
// 将任务加入 Lane 优先级队列
scheduler.lanePriorityQueue.enqueueTask(callback, nextLanes);
// 启动工作循环(未运行时)
if (!scheduler.isRunning) {
scheduler.isRunning = true;
requestHostCallback(workLoop); // 关联浏览器调度 API
}
}
/**
* 2. 工作循环(时间切片控制)
* @param {Function} hasTimeRemaining - 判断是否有剩余时间
*/
function workLoop(hasTimeRemaining: () => boolean) {
// 取出最高优先级任务
let task = scheduler.lanePriorityQueue.dequeueTask();
while (task) {
scheduler.currentTask = task;
// 执行任务(调和阶段的核心逻辑)
const didComplete = task.callback();
if (didComplete) {
// 任务完成(整棵树调和完毕),移除当前任务
scheduler.currentTask = null;
} else {
// 任务未完成(时间切片用尽),重新入队
scheduler.lanePriorityQueue.enqueueTask(task.callback, task.lane);
break; // 让出主线程
}
// 检查是否有剩余时间,无则暂停
if (!hasTimeRemaining()) break;
// 取下一个任务
task = scheduler.lanePriorityQueue.dequeueTask();
}
// 更新运行状态(无任务则停止)
scheduler.isRunning = !!task || scheduler.lanePriorityQueue.peek() !== null;
// 有剩余任务则继续调度下一帧
if (scheduler.isRunning) {
requestHostCallback(workLoop);
}
}
/**
* 3. 浏览器调度 API 封装(兼容不同环境)
* @param {Function} callback - 工作循环回调
*/
function requestHostCallback(callback: Function) {
// 优先使用 requestIdleCallback(浏览器空闲时执行)
if (typeof requestIdleCallback === 'function') {
requestIdleCallback((deadline) => {
callback(() => deadline.timeRemaining() > 0);
});
} else {
// 降级使用 setTimeout(模拟时间切片)
setTimeout(() => {
callback(() => true); // 假设始终有时间(简化)
}, 1);
}
}
/**
* 4. 调和任务核心回调(连接调度器与调和阶段)
* @param {FiberRoot} root - 根节点
* @param {Lane} lane - 当前任务的 Lane
* @returns {boolean} 是否完成调和
*/
function performConcurrentWorkOnRoot(root: FiberRoot, lane: Lane): boolean {
// 设置当前渲染的 Lane(供 beginWork 筛选节点)
renderLanes = lane;
// 执行并发工作循环(可中断)
workLoopConcurrent();
// 检查是否调和完成
if (nextUnitOfWork === null) {
// 调和完成,进入提交阶段
commitRoot(root);
return true; // 任务完成
}
return false; // 任务未完成(时间切片用尽)
}
三、防饿死机制(低优先级任务降级与重试)
通过 “打断计数” 和 “Lane 升级” 确保低优先级任务不被长期忽略,核心伪代码如下:
1. 任务打断计数与 Lane 升级
// 扩展 Task 类,增加打断计数
class Task {
constructor(id, callback, lane, expirationTime) {
this.id = id;
this.callback = callback;
this.lane = lane;
this.expirationTime = expirationTime;
this.priorityLevel = getSchedulerPriorityForLane(lane);
this.interruptionCount = 0; // 被打断次数
}
}
// 工作循环中更新打断计数
function workLoop(hasTimeRemaining: () => boolean) {
let task = scheduler.lanePriorityQueue.dequeueTask();
while (task) {
scheduler.currentTask = task;
const didComplete = task.callback();
if (!didComplete) {
// 任务被打断,计数+1
task.interruptionCount++;
// 检查是否需要升级 Lane(防饿死)
const upgradedLane = upgradeLaneIfNeeded(task.lane, task.interruptionCount);
task.lane = upgradedLane;
task.priorityLevel = getSchedulerPriorityForLane(upgradedLane);
// 重新入队(升级后优先级提高)
scheduler.lanePriorityQueue.enqueueTask(task.callback, task.lane);
break;
}
scheduler.currentTask = null;
if (!hasTimeRemaining()) break;
task = scheduler.lanePriorityQueue.dequeueTask();
}
// ... 调度状态更新
}
/**
* 2. Lane 升级逻辑(核心防饿死)
* @param {Lane} lane - 当前任务的 Lane
* @param {number} count - 被打断次数
* @returns {Lane} 升级后的 Lane
*/
function upgradeLaneIfNeeded(lane: Lane, count: number): Lane {
// 低优先级任务被打断 5 次,升级为普通优先级
if (lane === LowLane && count >= 5) {
return NormalLane;
}
// 普通优先级被打断 3 次,升级为用户阻塞级
if (lane === NormalLane && count >= 3) {
return UserBlockingLane;
}
// 其他优先级暂不升级
return lane;
}
3. 过期时间兜底(强制执行)
// 工作循环中检查任务是否过期
function workLoop(hasTimeRemaining: () => boolean) {
let task = scheduler.lanePriorityQueue.dequeueTask();
while (task) {
const now = performance.now();
// 任务过期(超过 expirationTime),强制执行(忽略时间切片)
const forceExecute = now > task.expirationTime;
scheduler.currentTask = task;
const didComplete = task.callback();
if (!didComplete && !forceExecute) {
// 未过期且被打断,重新入队
task.interruptionCount++;
const upgradedLane = upgradeLaneIfNeeded(task.lane, task.interruptionCount);
task.lane = upgradedLane;
task.priorityLevel = getSchedulerPriorityForLane(upgradedLane);
scheduler.lanePriorityQueue.enqueueTask(task.callback, task.lane);
break;
} else if (!didComplete && forceExecute) {
// 已过期,继续执行(不让出主线程)
continue;
}
scheduler.currentTask = null;
if (!forceExecute && !hasTimeRemaining()) break;
task = scheduler.lanePriorityQueue.dequeueTask();
}
// ... 调度状态更新
}
四、高优先级任务打断逻辑
当新的高优先级任务入队时,中断当前低优先级任务,优先执行高优先级任务:
/**
* 高优先级任务打断入口(如用户输入触发)
* @param {FiberRoot} root - 根节点
* @param {Lane} highLane - 高优先级 Lane
*/
function interruptLowPriorityWork(root: FiberRoot, highLane: Lane) {
// 1. 检查当前是否有低优先级任务在执行
if (scheduler.currentTask && !isHigherPriorityLane(scheduler.currentTask.lane, highLane)) {
// 2. 标记当前任务为未完成,保存进度(nextUnitOfWork)
const currentTask = scheduler.currentTask;
// 3. 中断当前任务,将其重新入队(保留打断计数)
scheduler.lanePriorityQueue.enqueueTask(currentTask.callback, currentTask.lane);
scheduler.currentTask = null;
}
// 4. 调度高优先级任务(立即执行)
scheduleUpdateOnFiber(root.current, highLane);
}
/**
* 判断 Lane 优先级高低(数值越小优先级越高)
* @param {Lane} currentLane - 当前任务 Lane
* @param {Lane} newLane - 新任务 Lane
* @returns {boolean} currentLane 是否比 newLane 高优先级
*/
function isHigherPriorityLane(currentLane: Lane, newLane: Lane): boolean {
return currentLane < newLane;
}
React 源码中几乎所有核心函数(如 performUnitOfWork、beginWork、commitMutationEffects)的入参都是单个 Fiber 节点,而非 “整棵 Fiber 树”。函数通过节点的链表指针(child/sibling/return)“局部遍历”,间接实现对整棵树的操作。
典型函数示例:
// 1. 调和阶段核心函数:仅操作当前节点,通过指针找下一个节点
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// 处理当前节点(局部操作)
const next = beginWork(unitOfWork.alternate, unitOfWork, renderLanes);
// 通过 child/sibling/return 指针找下一个节点(无需全局树引用)
if (next) return next;
let completedWork = unitOfWork;
while (completedWork) {
completeUnitOfWork(completedWork); // 处理当前节点的完成逻辑
if (completedWork.sibling) return completedWork.sibling; // 找兄弟节点
completedWork = completedWork.return; // 回溯到父节点
}
return null;
}
// 2. 提交阶段核心函数:通过副作用链表遍历,仍以单个节点为单位
function commitMutationEffects(root: FiberRoot) {
let nextEffect = root.finishedWork.firstEffect; // 从第一个副作用节点开始
while (nextEffect) {
commitMutationEffectsOnFiber(nextEffect, root); // 处理当前副作用节点
nextEffect = nextEffect.nextEffect; // 通过链表找下一个节点
}
}
源码结构
操作局部 Fiber 节点,而非整体
核心原因:
- 整棵 Fiber 树是动态构建的(尤其并发模式下可能被中断、复用),无法在函数调用时传递 “完整树引用”;
- 以 “单个节点 + 指针遍历” 的方式,既能支持任务中断(保存当前节点即可),又能减少内存占用(无需加载整棵树到函数上下文)。
“优先级队列 + 时间切片” 与 “局部节点操作” 的配合
优先级队列(小根堆)存储的是 “根节点级别的更新任务”,而非单个 Fiber 节点。但任务执行时,仍通过 “局部节点遍历” 完成整棵树的调和:
-
队列存储的是 “根任务” :队列中的每个任务关联的是
FiberRoot(根节点)和对应的lane,例如:// 任务队列中的元素(简化) const task = { root: fiberRoot, // 根节点引用 lane: SyncLane, // 优先级 callback: () => performConcurrentWorkOnRoot(fiberRoot, SyncLane) // 任务回调 }; -
任务执行时的局部遍历:回调函数
performConcurrentWorkOnRoot会从根节点的current.child开始,通过performUnitOfWork逐个处理节点,直到整棵树调和完成或时间切片用尽:function performConcurrentWorkOnRoot(root, lane) { // 从根节点的子节点开始调和(局部遍历起点) nextUnitOfWork = root.current.child; workLoopConcurrent(); // 循环处理单个节点 // ... } -
时间切片中断的本质:当
shouldYield()触发时,仅需保存当前正在处理的nextUnitOfWork(单个 Fiber 节点),即可暂停任务;恢复时从该节点继续遍历,无需关心整棵树的状态。
React 源码的 “局部优先” 设计哲学
- 数据层面:通过 Fiber 节点的链表指针(
child/sibling/return)和双缓存引用(alternate),实现 “局部访问全局”; - 函数层面:核心逻辑函数仅接收单个 Fiber 节点,通过指针遍历完成整棵树的操作,避免依赖 “全局树引用”;
- 调度层面:优先级队列管理根节点级任务,执行时通过局部节点遍历实现调和,时间切片中断仅需保存当前节点,高效且灵活。
这种设计是 React 支持 “并发渲染” 和 “时间切片” 的关键 —— 如果函数需要操作整棵树,任务中断和恢复将变得异常复杂(需保存整棵树的状态),而 “局部节点操作 + 指针遍历” 完美解决了这一问题。
层级结构
1. 什么是 React Fiber?它解决了什么问题?
React Fiber 是 React 16 引入的新协调引擎,本质是对 "工作单元"(Fiber 节点)的重新设计,核心是将渲染工作拆分为可中断、可恢复、可优先级排序的小单元。
它解决的核心问题是:
- 同步渲染的性能瓶颈:React 15 及之前使用栈协调器(Stack Reconciler),渲染过程是同步且不可中断的。当组件树庞大时,长时间占用主线程会导致 UI 卡顿(无法响应用户输入、动画等)。
- 任务优先级调度:无法区分更新任务的优先级(如用户输入需要立即响应,而列表渲染可延迟),导致高优任务被低优任务阻塞。
Fiber 的解决方案:
- 拆分工作单元:每个 Fiber 节点对应一个组件,作为最小工作单元,可独立处理。
- 链表结构支持中断 / 恢复:通过
child(子节点)、sibling(兄弟节点)、return(父节点)指针构建树状链表,遍历过程中可随时暂停(保存nextUnitOfWork),恢复时从该节点继续。 - 优先级驱动调度:结合 Lane 模型标记任务优先级,高优任务可抢占低优任务,优先完成渲染。
2. React 的渲染流程分为哪几个阶段?各阶段的核心工作是什么?
React 渲染流程分为调度阶段(Schedule) 、协调阶段(Reconciliation) 、提交阶段(Commit) 三个核心阶段,各阶段职责如下:
-
调度阶段(Schedule) :
-
核心工作:确定更新任务的优先级,决定何时执行该任务。
-
触发时机:当组件调用
setState、useState等触发更新时,通过scheduleUpdateOnFiber启动。 -
关键逻辑:
- 为更新任务分配 Lane(优先级标记,如同步优先级
SyncLane、用户阻塞优先级UserBlockingLane)。 - 向上遍历至根节点,标记
rootFiber.pendingLanes(汇总所有待处理优先级)。 - 通过
ensureRootIsScheduled将根节点任务注册到 Scheduler,根据最高优先级安排执行时机。
- 为更新任务分配 Lane(优先级标记,如同步优先级
-
-
协调阶段(Reconciliation,可中断) :
-
核心工作:找出前后 DOM 树的差异(Diff),标记需要执行的副作用(如新增、删除、更新)。
-
关键逻辑:
- 以
rootFiber为起点,通过performUnitOfWork遍历 Fiber 树,每次处理一个nextUnitOfWork(当前工作单元)。 - 构建
workInProgress树(双缓存机制中的 "工作树"):对比current树(当前 DOM 对应的 Fiber 树)与新状态,复用可复用节点,创建 / 删除节点,调整child/sibling/return指针。 - 标记副作用:为需要变更的节点设置
effectTag(如Placement插入、Update更新、Deletion删除),并通过completeUnitOfWork串联成effectList(副作用链表)。 - 优先级筛选:仅处理与当前
renderLanes(调度优先级)匹配的节点,低优节点直接跳过。
- 以
-
可中断性:通过
shouldYield()检查时间切片(默认 5ms),超时则暂停,保存nextUnitOfWork,让出主线程。
-
-
提交阶段(Commit,不可中断) :
-
核心工作:执行
effectList中的副作用,将协调阶段计算的差异应用到真实 DOM,并执行生命周期 / Effect 钩子。 -
关键逻辑:
-
遍历根节点的
firstEffect(effectList表头),通过nextEffect指针依次处理所有带effectTag的节点:commitMutationEffects:执行 DOM 操作(增删改)。commitLayoutEffects:执行useLayoutEffect的回调和清理函数,更新 refs。
-
切换双缓存:将
workInProgress树设置为新的current树(root.current = finishedWork)。 -
异步执行
useEffect:通过schedulePassiveEffects在 DOM 变更后异步触发useEffect的回调和清理。
-
-
3. 什么是 Lane 模型?它在 React 调度中起到什么作用?
Lane 是个二进制值,位运算来表示优先级,并且很方便合并。
Schedule 阶段,节点更新时回溯到根节点,此为局部遍历,这个过程构建的优先级队列类似小根堆,头为优先级最高的节点。
Reconciliation 阶段,不断取出当前的头节点,执行并记录 Lane 值为 RenderLane ,当这个值变化,意味着一系列同优先级的 Node 已经执行完,立刻执行commit 更新。 然后执行后面优先级的。
Reconciliation 阶段会被 ShouldYield 打断,可能是时间切片,也可能是被插入了高优节点,比如当前 renderLane 小于插入的, 当前就被打断,优先执行高优。
4. React 如何实现任务的中断与恢复?为什么 Commit 阶段不可中断?
答案:
(1)任务中断与恢复的实现(仅发生在 Reconciliation 阶段)
核心依赖 Fiber 的链表结构和nextUnitOfWork指针:
- 中断:协调阶段通过
shouldYield()检查是否超过时间切片(5ms)或有高优任务插入。若需中断,React 会保存当前正在处理的 Fiber 节点为nextUnitOfWork,然后退出工作循环,释放主线程。 - 恢复:当主线程空闲或高优任务处理完成后,调度器会重新启动工作循环,从
nextUnitOfWork继续遍历 Fiber 树,处理剩余工作单元。
原理:Fiber 节点通过child/sibling/return指针形成链表,遍历过程无需依赖调用栈(区别于 React 15 的栈协调器),只需记录下一个待处理节点即可恢复,实现轻量中断。
(2)Commit 阶段不可中断的原因
Commit 阶段的核心是执行 DOM 操作和副作用(如useLayoutEffect),若中断会导致:
- DOM 不一致:DOM 操作是原子性的(如插入节点到一半被中断),会导致 UI 呈现不完整状态,影响用户体验。
- 副作用混乱:
useLayoutEffect的清理函数和回调函数需要在 DOM 变更后同步执行,若中断可能导致清理 / 执行顺序错误,引发内存泄漏或逻辑异常。
因此,Commit 阶段被设计为不可中断的同步过程,确保 DOM 和副作用的一致性。
5. 什么是双缓存机制?React 中如何利用双缓存实现 DOM 更新?
双缓存是 React 用于高效更新 DOM 的优化策略,核心是维护两棵 Fiber 树:current树和workInProgress树。
-
双缓存定义:
current树:与当前页面 DOM 对应的 Fiber 树,节点标记为current。workInProgress树:正在构建的新 Fiber 树,反映最新的组件状态,节点标记为workInProgress。
-
更新流程:
- 初始渲染时,React 构建
workInProgress树,完成后将其设置为current树(root.current = workInProgress),并渲染到 DOM。 - 触发更新时,React 以
current树为模板,重新构建workInProgress树:复用无需变更的节点(复制current节点属性到workInProgress),创建 / 删除需要变更的节点。 - 协调阶段完成后,Commit 阶段将
workInProgress树切换为新的current树,一次性将差异应用到 DOM。
- 初始渲染时,React 构建
-
优势:
- 避免频繁操作 DOM:在
workInProgress树中完成所有计算,最终只执行一次 DOM 更新,减少重绘重排。 - 确保渲染一致性:切换前
current树始终与 DOM 保持同步,避免中间状态暴露给用户。
- 避免频繁操作 DOM:在
6. useEffect 与 useLayoutEffect 的执行时机有何不同?为什么会有这种区别?
两者都是处理副作用的 Hook,但执行时机和场景不同,核心区别源于 React 的渲染阶段划分:
(1)执行时机
-
useLayoutEffect:在Commit 阶段的commitLayoutEffects步骤同步执行(DOM 变更后,浏览器绘制前)。- 具体流程:DOM 操作(
commitMutationEffects)→ 执行useLayoutEffect的清理函数和回调 → 浏览器绘制。
- 具体流程:DOM 操作(
-
useEffect:在Commit 阶段结束后异步执行(浏览器绘制后)。- 具体流程:DOM 操作 → 浏览器绘制 → 异步执行
useEffect的清理函数和回调(通过schedulePassiveEffects调度)。
- 具体流程:DOM 操作 → 浏览器绘制 → 异步执行
(2)区别原因
useLayoutEffect同步执行:用于需要在 DOM 变更后、绘制前读取 / 修改 DOM 的场景(如计算元素位置并调整样式),避免因异步执行导致的 "闪烁"(先绘制旧样式,再更新新样式)。useEffect异步执行:大多数副作用(如数据请求、事件监听)无需阻塞浏览器绘制,异步执行可避免占用主线程,提升用户体验(尤其是动画和交互场景)。
7. React 的并发模式(Concurrent Mode)是什么?它与同步模式的核心区别是什么?
答案:并发模式是 React 的一种渲染模式,允许 React 中断、暂停、恢复甚至放弃渲染工作,核心是支持 "非阻塞渲染",是 Fiber 架构的重要应用。
(1)核心特性
- 任务优先级:高优任务(如用户输入)可抢占低优任务(如列表渲染),优先完成。
- 可中断渲染:Reconciliation 阶段可被中断,避免长时间阻塞主线程。
- ** Suspense 支持 **:配合
Suspense实现数据请求与 UI 渲染的协调(如 "加载中" 状态)。
(2)与同步模式的核心区别
| 维度 | 同步模式(React 15 及之前) | 并发模式(React 16+) |
|---|---|---|
| 渲染过程 | 同步不可中断,一旦开始必须完成 | 可中断、可恢复,按优先级调度 |
| 主线程占用 | 长时间占用,可能导致 UI 卡顿 | 时间切片内执行,及时让出主线程 |
| 任务优先级 | 无优先级区分,按触发顺序执行 | 基于 Lane 模型,高优任务可抢占 |
| 适用场景 | 简单 UI,无复杂交互 / 动画 | 复杂 UI,需流畅响应用户输入 / 动画 |
8. 什么是 effectList?它在 Commit 阶段起到什么作用?
effectList 是协调阶段收集的 "副作用链表",存储所有需要在 Commit 阶段执行 DOM 操作或副作用的 Fiber 节点,通过 Fiber Node 的属性 :firstEffect(表头)和nextEffect(指针)串联。
(1)构建过程
-
在 Reconciliation 阶段的
completeUnitOfWork中,React 会将带有effectTag(如Update、Placement)的 Fiber 节点加入 effectList:- 若当前节点有
effectTag,则将其链接到父节点的 effectList 中。 - 最终,根节点的
firstEffect指向整个链表的头部,nextEffect依次指向后续节点。
- 若当前节点有
(2)在 Commit 阶段的作用
- 精准执行 DOM 操作:Commit 阶段无需遍历整个 Fiber 树,只需从根节点
firstEffect开始,沿nextEffect遍历 effectList,仅对带effectTag的节点执行对应操作(如更新属性、插入节点),减少不必要的遍历。 - 隔离不同优先级副作用:高优任务和低优任务的 effectList 相互独立,Commit 阶段仅处理当前优先级对应的链表,确保高优更新先于低优更新生效。
9. useTransition 的作用是什么?它是如何实现低优先级更新的?
(1)作用
useTransition用于标记低优先级更新,避免其阻塞高优任务(如用户输入、动画),实现 "非阻塞渲染"。例如,在搜索框输入时,输入框状态(高优)即时更新,而搜索结果列表(低优)可延迟渲染,确保输入流畅。
(2)实现原理(基于 Lane 模型)
- 标记低优 Lane:
useTransition触发的更新会被分配低优先级 Lane(如LowLane),区别于setState的同步 Lane(SyncLane)。 - 优先级筛选:协调阶段,
renderLanes优先处理高优 Lane(输入框更新),低优 Lane 的节点会被shouldProcessUnitOfWork跳过。 - 任务中断:若低优更新正在协调,此时有高优任务插入,React 会中断低优任务,优先处理高优任务,完成后再恢复低优任务的协调。
- 防饿死机制:若低优任务被多次打断,React 会通过
upgradeLaneIfNeeded升级其 Lane 优先级,避免永远无法执行。
10. React 中如何判断一个组件是否需要更新?(涉及 Reconciliation 阶段的 Diff 逻辑)
Reconciliation 阶段通过 "类型判断" 和 "属性对比" 决定组件是否需要更新,核心逻辑在beginWork中实现,不同组件类型的判断逻辑不同:
(1)原生 DOM 组件(HostComponent,如div、input)
-
判断依据:
elementType(标签名)是否相同 +pendingProps与memoizedProps是否存在差异。- 若标签名不同:销毁旧节点,创建新节点(
effectTag = Placement)。 - 若标签名相同:对比
pendingProps和memoizedProps,若有差异则标记Update,更新 DOM 属性;否则复用节点,跳过子节点协调。
- 若标签名不同:销毁旧节点,创建新节点(
(2)函数组件(FunctionComponent)
- 基础判断:
pendingProps与memoizedProps是否相同(浅对比)。若相同,复用memoizedState,跳过更新;若不同,执行组件函数,重新计算 Hook 状态。 - 优化判断(
React.memo):若组件被memo包裹,会通过自定义比较函数(或浅对比)判断prevProps与nextProps,若相同则跳过更新。
(3)类组件(ClassComponent)
- 基础判断:
pendingProps与memoizedProps是否相同。 - 额外判断:调用
shouldComponentUpdate(若定义),返回false则跳过更新;否则执行componentWillUpdate,继续协调子节点。
props 和 hooks 的存储与执行
对应 Fiber Node 的以下字段
// 3. 状态与更新相关(管理组件的 props 和状态)
pendingProps: { value: 'newInputValue', onChange: () => {} }, // 待应用的新 props(用户输入后)
memoizedProps: { value: 'oldInputValue', onChange: () => {} }, // 已缓存的旧 props
memoizedState: {
// 第一个 useState 钩子
baseState: 'initialValue',
queue: { pending: null }, // 该钩子的更新队列
next: {
// 第二个 useEffect 钩子
tag: 3, // Passive 标记(useEffect)
create: () => { console.log('effect'); return () => {}; }, // 回调函数
destroy: null, // 清理函数(尚未执行)
deps: [/* 依赖数组 */],
next: null // 无更多钩子
}
}
updateQueue: { // 待执行的更新队列(用户输入触发的更新)
shared: {
pending: {
lane: 0b00000001, // SyncLane(同步优先级)
action: (prev) => ({ ...prev, value: 'newInputValue' }),
next: null
}
}
},
1. pendingProps 新 Props”(待应用) and memoizedProps 旧 Props”(已缓存)
核心作用:存储组件的 “新 Props”(待应用)和 “旧 Props”(已缓存),Reconciliation 阶段通过对比两者差异,决定是否需要更新组件。
业务场景示例:用户在登录页输入用户名,触发 Input 组件 Props 更新
-
初始状态(组件挂载后):
memoizedProps:{ value: '', onChange: handleInputChange, placeholder: '请输入用户名' }(已缓存的旧 Props)pendingProps:null(无待应用更新)
-
用户输入 “zhangsan” 后(触发更新):
pendingProps:{ value: 'zhangsan', onChange: handleInputChange, placeholder: '请输入用户名' }(新 Props 被传入)
-
Reconciliation 阶段处理后:
memoizedProps被更新为pendingProps的值(旧 Props 替换为新 Props)pendingProps重置为null(更新已应用)
伪代码(Props 更新核心逻辑) :
// Reconciliation阶段处理Props更新(beginWork函数核心逻辑片段)
function beginWork(current, workInProgress, renderLanes) {
const nextProps = workInProgress.pendingProps; // 取待应用的新Props
const prevProps = workInProgress.memoizedProps; // 取已缓存的旧Props
// 对比Props差异(浅对比,类组件用shouldComponentUpdate,函数组件用memo)
if (shallowEqual(nextProps, prevProps)) {
// Props无变化,复用旧状态,跳过后续调和
workInProgress.memoizedState = current.memoizedState;
return null;
}
// Props有变化,更新缓存的Props,继续调和子节点
workInProgress.memoizedProps = nextProps;
// 后续处理子节点调和...
return reconcileChildren(current, workInProgress, nextProps.children, renderLanes);
}
2. memoizedState:Hook 链表的 “总入口”
核心作用:存储组件的 Hook 链表(useState/useEffect/useRef 等),每个 Hook 是链表的一个节点,通过next指针串联,React 通过遍历链表执行 Hook 逻辑。
业务场景示例:登录页组件有 2 个 useState(用户名、密码)和 2 个 useEffect(监听输入、提交请求)
// 组件代码(业务场景)
function LoginForm() {
// Hook1:用户名状态
const [username, setUsername] = useState('');
// Hook2:密码状态
const [password, setPassword] = useState('');
// Hook3:监听用户名输入,打印日志(无依赖)
useEffect(() => {
console.log('用户名变化:', username);
}, [username]);
// Hook4:提交表单后请求接口(依赖username和password)
useEffect(() => {
const fetchLogin = async () => {
if (username && password) await api.login(username, password);
};
fetchLogin();
return () => console.log('清理请求'); // 清理函数
}, [username, password]);
return <input onChange={(e) => setUsername(e.target.value)} />;
}
对应 Fiber 节点 memoizedState 值(Hook 链表结构) :
workInProgress.memoizedState = {
// Hook1:useState(username)
tag: 0, // Hook类型:0=useState
baseState: '', // 初始值/当前状态
baseQueue: null, // 该Hook的更新队列(未触发更新时为null)
next: {
// Hook2:useState(password)
tag: 0,
baseState: '',
baseQueue: null,
next: {
// Hook3:useEffect(监听用户名)
tag: 3, // Hook类型:3=Passive(useEffect)
create: () => { console.log('用户名变化:', username); }, // 执行函数
destroy: null, // 无清理函数
deps: [username], // 依赖数组
next: {
// Hook4:useEffect(登录请求)
tag: 3,
create: () => { /* fetchLogin逻辑 */ },
destroy: () => console.log('清理请求'), // 清理函数
deps: [username, password], // 依赖数组
next: null // 链表结束
}
}
}
};
伪代码(Hook 链表遍历逻辑) :
// Reconciliation阶段遍历Hook链表(updateFunctionComponent函数片段)
function updateFunctionComponent(workInProgress, Component, nextProps) {
const hookQueue = workInProgress.memoizedState; // 获取Hook链表头
let currentHook = hookQueue;
let index = 0;
// 执行组件函数,依次处理每个Hook
Component();
// 遍历Hook链表,执行更新逻辑(如useState计算新状态,useEffect对比依赖)
while (currentHook) {
switch (currentHook.tag) {
case 0: // useState
updateStateHook(currentHook); // 处理状态更新
break;
case 3: // useEffect
updateEffectHook(currentHook, renderLanes); // 对比依赖,标记副作用
break;
}
currentHook = currentHook.next; // 移动到下一个Hook
index++;
}
}
3. updateQueue:组件的 “更新任务队列”
核心作用:存储组件触发的所有待执行更新(如 setState、useState 的更新函数),每个更新包含优先级(Lane)、更新逻辑(action),Reconciliation 阶段会消费队列生成新状态。
业务场景示例:用户连续输入 “zh”→“zha”→“zhan”,触发 3 次 username 更新(同步优先级)
// 对应Fiber节点updateQueue值(更新队列结构)
workInProgress.updateQueue = {
shared: {
pending: {
// 第3次更新:输入“zhan”
lane: 0b00000001, // SyncLane(同步优先级,最高)
action: (prev) => prev + 'n', // 更新函数:前状态+“n”
next: {
// 第2次更新:输入“zha”
lane: 0b00000001,
action: (prev) => prev + 'a',
next: {
// 第1次更新:输入“zh”
lane: 0b00000001,
action: (prev) => prev + 'h',
next: null // 队列末尾
}
}
}
}
};
伪代码(更新队列消费逻辑) :
// 处理updateQueue,生成新状态(processUpdateQueue函数片段)
function processUpdateQueue(workInProgress, queue, renderLanes) {
const pendingUpdate = queue.shared.pending; // 取待执行更新队列
let newState = workInProgress.memoizedState.baseState; // 初始状态('')
// 遍历更新队列,依次执行更新函数(合并多次更新)
while (pendingUpdate) {
// 检查更新优先级是否匹配当前renderLanes(仅处理当前优先级更新)
if (isUpdatePriorityMatch(pendingUpdate.lane, renderLanes)) {
// 执行更新函数,计算新状态
newState = pendingUpdate.action(newState);
}
pendingUpdate = pendingUpdate.next; // 移动到下一个更新
}
// 更新memoizedState的baseState(缓存新状态)
workInProgress.memoizedState.baseState = newState;
// 清空更新队列(已消费)
queue.shared.pending = null;
return newState;
}
层级结构
为什么 Hook 不能在 if/for 循环、条件判断中使用?谁来检查?
核心原因是「Hook 依赖 memoizedState 的链表结构按顺序遍历,打乱顺序会导致链表错位」,检查由「ESLint 插件(eslint-plugin-react-hooks)和 React 运行时」共同完成。
-
从 Fiber 字段角度解释:组件的 Hook 会按调用顺序存入
memoizedState链表(第 1 个 useState 是链表头,第 2 个 useEffect 是下一个节点...)。React 在 Reconciliation 阶段遍历链表时,通过 “顺序索引” 匹配 Hook(第 1 次遍历对应第 1 个 Hook,第 2 次对应第 2 个) ,不依赖名称或标识。若在 if 中使用 Hook:
if (username) { useEffect(() => {}, [username]); // 条件成立时才调用,顺序打乱 }首次渲染时
username为 true,Hook 链表有 4 个节点(2 个 useState+2 个 useEffect);二次渲染时username为 false,Hook 调用顺序变为 2 个 useState,遍历链表时会把第 3 个节点(原本的 useEffect)当成第 3 个 Hook,导致类型不匹配、依赖数组错误。 -
检查机制:
- ESLint 插件(
react-hooks/rules-of-hooks):编译时检查,通过追踪 Hook 调用顺序,若发现嵌套在条件 / 循环中,直接报错(红色波浪线); - React 运行时:渲染时遍历 Hook 链表,对比当前 Hook 的
tag(类型)与上次渲染的链表节点是否一致,若不一致则抛出错误(Invalid hook call. Hooks can only be called inside of the body of a function component.)。
- ESLint 插件(
memoizedState 和 updateQueue 的区别是什么?分别存储什么?
memoizedState 是已经生效的状态;updateQueue 是待更新的任务队列,队列所有任务的 Lane 会聚合成 fiber Node 的 lane。
三者流转关系:updateQueue → lane → memoizedState。
新更新入队(updateQueue添加update)→ 聚合优先级到lane → reconcile阶段按lane筛选update执行 → 生成新状态写入memoizedState → 清理lane中已处理优先级
核心字段结构定义
const fiberNode = {
// 1. memoizedState:缓存「当前生效的状态/Hook链表」(已落地的状态)
memoizedState: {
// Hook1:useState(用户名状态,当前生效值)
tag: 0, // 0=useState
baseState: 'zhangsan', // 已生效的最终状态(经updateQueue处理后)
baseQueue: null,
next: {
// Hook2:useEffect(依赖数组,当前生效的依赖)
tag: 3, // 3=useEffect
create: () => {},
destroy: () => {},
deps: ['zhangsan'], // 已生效的依赖配置
next: null
}
},
// 2. updateQueue:存储「待执行的更新任务队列」(未落地的变更)
updateQueue: {
shared: {
pending: {
// 待处理的更新任务1(用户输入触发,同步优先级)
lane: 0b00000001, // SyncLane
action: (prev) => prev + '1', // 更新逻辑
next: {
// 待处理的更新任务2(低优任务,useTransition触发)
lane: 0b00010000, // LowLane
action: (prev) => prev + '2',
next: null
}
}
}
},
// 3. lane:标记「当前节点待处理更新的优先级集合」(聚合自updateQueue)
lane: 0b00010001 // 聚合两个update的lane(0b00000001 | 0b00010000)
};
关键流程伪代码(体现 lane、updateQueue、memoizedState 的联动)
/**
* 1. 更新入队:新update添加到updateQueue,同时聚合lane
* 回答:fiber.lane不是updateQueue直接更新,而是由updateQueue中的update.lane聚合而来
*/
function enqueueUpdate(fiber, update) {
// 步骤1:将update加入updateQueue队列(链表结构)
const queue = fiber.updateQueue;
if (!queue.shared.pending) {
update.next = update; // 初始化循环链表
} else {
update.next = queue.shared.pending.next;
queue.shared.pending.next = update;
}
queue.shared.pending = update;
// 步骤2:聚合update的lane到fiber.lane(核心:lane由update队列间接影响)
fiber.lane |= update.lane; // 位或运算,保留所有待处理优先级
// 步骤3:向上冒泡更新根节点pendingLanes,通知调度器
propagateUpdateToRoot(fiber);
}
/**
* 2. Reconcile阶段:处理updateQueue,更新memoizedState和lane
* 回答:reconcile执行完updateQueue后,会更新memoizedState(写入新状态)和lane(清理已处理优先级)
*/
function processUpdateQueue(fiber, queue, renderLanes) {
let pendingUpdate = queue.shared.pending; // 待处理更新队列
let newBaseState = fiber.memoizedState.baseState; // 初始生效状态
let remainingLanes = 0; // 未处理的优先级集合
// 遍历updateQueue,按renderLanes筛选并执行更新
while (pendingUpdate) {
const currentUpdate = pendingUpdate;
// 检查update优先级是否匹配当前调度的renderLanes
if (isUpdatePriorityMatch(currentUpdate.lane, renderLanes)) {
// 执行更新函数,合并计算新状态
newBaseState = currentUpdate.action(newBaseState);
} else {
// 不匹配当前优先级,保留其lane(后续调度处理)
remainingLanes |= currentUpdate.lane;
}
pendingUpdate = pendingUpdate.next;
}
// 步骤1:更新memoizedState,写入新的生效状态
fiber.memoizedState.baseState = newBaseState;
// 步骤2:更新lane,仅保留未处理的优先级(清理已处理的)
fiber.lane = remainingLanes;
// 步骤3:清空已处理的updateQueue(未处理的更新会重新入队)
queue.shared.pending = remainingLanes ? pendingUpdate : null;
return newBaseState;
}
useEffect 的清理函数是如何存储和执行的?和 memoizedState 有什么关系?
useEffect 的清理函数(destroy)存储在memoizedState的 Hook 节点中,执行时机由 “依赖变化” 和 “组件卸载” 触发,流程如下:
-
存储:每个 useEffect 对应
memoizedState链表中的一个 Hook 节点(tag: 3),清理函数通过destroy字段存储:// useEffect对应的Hook节点 { tag: 3, create: () => { /* 执行函数 */ }, destroy: () => console.log('清理'), // 清理函数存储在这里 deps: [username], next: null } -
执行时机:
- 依赖变化时:Reconciliation 阶段对比
deps数组,若依赖变化,先执行上一次 Hook 节点的destroy(清理旧 effect),再执行新的create(创建新 effect),并更新destroy字段为新的清理函数; - 组件卸载时:Commit 阶段遍历
effectList(副作用链表),执行所有未执行的destroy函数(标记为PassiveUnmount的 effect)。
- 依赖变化时:Reconciliation 阶段对比
-
与 memoizedState 的关系:清理函数是 Hook 节点的属性,随
memoizedState链表持久化存储,React 通过遍历链表找到对应 Hook,获取destroy函数执行。
多次调用 setState(或 useState 的更新函数),React 会如何处理?和 updateQueue 有什么关系?
React 会将多次更新合并为一个批次处理,核心依赖updateQueue的队列结构,流程如下:
- 多次更新触发时:每次调用 setState/useState,都会创建一个
update对象(含lane优先级、action更新函数),并加入updateQueue.shared.pending队列(形成链表),不会立即执行。 - 合并处理:Reconciliation 阶段消费
updateQueue时,会遍历队列中的所有update,按顺序执行action函数(合并多次更新,如setCount(c => c+1)执行 3 次,最终结果是 + 3,而非 + 1),生成最终新状态。 - 优先级控制:每个
update的lane字段标记优先级,消费队列时仅处理当前renderLanes(当前调度优先级)匹配的update,高优先级更新(如用户输入)会打断低优先级更新,优先消费。
合成事件 SyntheticEvent
React 17 对合成事件系统做了重要调整:事件委托的目标从 document 改为了 React 根容器(即 ReactDOM.render 或 createRoot 挂载的容器节点) 。这一变化解决了多版本 React 共存、与其他框架事件冲突等问题,同时也影响了事件冒泡、捕获的具体行为。
一、版本差异:事件委托目标的变迁
| 版本 | 事件委托目标 | 核心原因 |
|---|---|---|
| React 16 及之前 | document 节点 | 早期设计为简化事件管理,所有 React 事件统一由顶层 document 监听 |
| React 17 及之后 | React 根容器(如 #root 节点) | 1. 支持多版本 React 共存(不同版本事件互不干扰);2. 与其他框架(如 Vue)事件系统兼容;3. 更符合直觉的事件冒泡范围(不会穿透到 document 层面) |
二、合成事件系统完整实现原理(含 React 17+ 调整)
1. 事件初始化:绑定到根容器(React 17+)
// React 17+ 事件委托初始化(绑定到根容器)
function initEventDelegation(rootContainer) {
// 事件类型映射:React 事件名 → 原生事件名(部分事件名有差异)
const eventTypeMap = {
onClick: 'click',
onInput: 'input',
onChange: 'change', // 特殊:onChange 可能对应 input/change 等原生事件
onClickCapture: 'click' // 捕获阶段事件
};
// 为根容器绑定所有支持的原生事件
Object.entries(eventTypeMap).forEach(([reactEventType, nativeEventType]) => {
// 区分捕获/冒泡阶段(带 Capture 后缀的事件用捕获模式)
const useCapture = reactEventType.endsWith('Capture');
rootContainer.addEventListener(
nativeEventType,
(nativeEvent) => handleNativeEvent(nativeEvent, rootContainer),
useCapture // 捕获阶段事件用 true,冒泡用 false
);
});
}
2. 事件触发:从目标元素到根容器的「Fiber 树遍历」
当原生事件触发(如用户点击按钮),React 会执行以下步骤:
// 原生事件触发后的核心处理逻辑
function handleNativeEvent(nativeEvent, rootContainer) {
// 步骤1:找到原生事件对应的 DOM 目标节点
const targetDOMNode = nativeEvent.target;
// 步骤2:从 DOM 节点映射到对应的 Fiber 节点(关键:Fiber 与 DOM 的关联)
const targetFiber = getFiberFromDOMNode(targetDOMNode);
if (!targetFiber) return; // 非 React 管理的节点,不处理
// 步骤3:收集事件回调(从目标 Fiber 向上遍历至根容器对应的 Fiber)
const eventCallbacks = [];
const reactEventType = getReactEventType(nativeEvent.type); // 如 'click' → 'onClick'
let currentFiber = targetFiber;
while (currentFiber && currentFiber.stateNode !== rootContainer) {
// 从 Fiber 的 memoizedProps 中获取事件回调(区分捕获/冒泡)
const callback = currentFiber.memoizedProps[reactEventType];
if (callback) {
eventCallbacks.push({ callback, fiber: currentFiber });
}
currentFiber = currentFiber.return; // 向上遍历父 Fiber
}
// 步骤4:创建合成事件对象(SyntheticEvent)
const syntheticEvent = createSyntheticEvent(nativeEvent);
// 步骤5:执行回调(注意:捕获阶段是从根到目标,冒泡是从目标到根,这里需反转顺序)
const isCapture = reactEventType.endsWith('Capture');
if (isCapture) {
// 捕获阶段:从根到目标(正序执行)
eventCallbacks.forEach(({ callback }) => callback(syntheticEvent));
} else {
// 冒泡阶段:从目标到根(反转后执行)
eventCallbacks.reverse().forEach(({ callback }) => callback(syntheticEvent));
}
// 步骤6:清理合成事件(React 17 移除事件池后,此步骤简化)
cleanupSyntheticEvent(syntheticEvent);
}
3. 合成事件对象(SyntheticEvent)的设计细节
// 合成事件对象(React 17+ 移除事件池后版本)
class SyntheticEvent {
constructor(nativeEvent) {
this.nativeEvent = nativeEvent; // 原生事件引用
this.target = nativeEvent.target; // 事件目标(DOM 节点)
this.currentTarget = null; // 当前处理回调的 Fiber 对应的 DOM 节点(动态更新)
this.bubbles = nativeEvent.bubbles;
this.cancelable = nativeEvent.cancelable;
// ... 其他属性(如 type、timeStamp 等)
}
// 阻止合成事件冒泡(仅影响 React 合成事件,不直接阻止原生事件)
stopPropagation() {
this.isPropagationStopped = true; // 标记冒泡已停止
this.nativeEvent.stopPropagation(); // 同时阻止原生事件冒泡(可选,看版本)
}
// 阻止默认行为
preventDefault() {
this.defaultPrevented = true;
this.nativeEvent.preventDefault();
}
}
// 创建合成事件(React 17 移除事件池,每次创建新对象)
function createSyntheticEvent(nativeEvent) {
return new SyntheticEvent(nativeEvent);
}
// 清理合成事件(无需回收,交给 GC 处理)
function cleanupSyntheticEvent(syntheticEvent) {
// 仅重置必要属性,无需放回事件池
syntheticEvent.currentTarget = null;
}
4. Fiber 节点与事件回调的存储
事件回调存储在 Fiber 节点的 memoizedProps 中,与组件 props 一一对应:
// Fiber 节点结构(含事件回调)
const fiberNode = {
type: 'button', // 组件类型(如 'button' 或自定义组件)
memoizedProps: {
onClick: () => console.log('点击'), // 冒泡阶段回调
onClickCapture: () => console.log('捕获阶段点击'), // 捕获阶段回调
// 其他 props(如 className、style 等)
},
stateNode: document.querySelector('button'), // 对应的 DOM 节点
return: parentFiber, // 父 Fiber 节点(用于向上遍历)
// ... 其他 Fiber 字段(如 child、sibling 等)
};
层级结构
1. React 17 为什么将事件委托目标从 document 改为根容器?
- 多版本共存问题:若页面同时存在 React 16 和 React 17 两个版本,事件都委托到 document 会导致回调执行混乱(旧版本可能误处理新版本事件);
- 跨框架兼容性:与 Vue、jQuery 等其他框架共存时,事件不会穿透到 document 层面,减少冲突;
- 事件冒泡范围更合理:事件冒泡仅在 React 根容器内部,不会影响容器外的 DOM 节点,符合开发者直觉。
2. 合成事件与原生事件的执行顺序谁先谁后?
- React 16 及之前:合成事件委托到 document,原生事件若绑定在普通 DOM 节点上,会先执行原生事件,再执行合成事件(因事件冒泡到 document 时触发合成事件);若原生事件绑定在 document 上,会在合成事件之后执行(因合成事件先处理)。
- React 17 及之后:合成事件委托到根容器,原生事件若绑定在根容器内的节点,会先执行原生事件,再执行合成事件(冒泡到根容器时触发);若原生事件绑定在根容器外(如 body),则不会被 React 合成事件影响。
示例:
<div id="root">
<button id="btn">点击</button>
</div>
// React 组件(绑定合成事件)
function App() {
return <button onClick={() => console.log('合成事件')}>点击</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// 原生事件绑定
document.getElementById('btn').addEventListener('click', () => console.log('原生事件'));
// 执行顺序(React 17):原生事件 → 合成事件(因事件先冒泡到 btn,再到 root 容器)
3. 如何在 React 中同时使用合成事件和原生事件?有什么注意事项?
-
使用方式:原生事件可通过
ref获取 DOM 节点后绑定(如useEffect中调用addEventListener)。 -
注意事项:
- 合成事件的
stopPropagation无法阻止原生事件冒泡(需在原生事件中调用stopPropagation); - 原生事件需在组件卸载时清理(否则可能引发内存泄漏);
- 避免同时使用合成事件和原生事件处理同一行为(可能导致逻辑混乱)。
- 合成事件的
示例:
function MyComponent() {
const btnRef = useRef(null);
useEffect(() => {
const handleNativeClick = () => console.log('原生事件');
btnRef.current.addEventListener('click', handleNativeClick);
// 清理原生事件
return () => btnRef.current.removeEventListener('click', handleNativeClick);
}, []);
return <button ref={btnRef} onClick={() => console.log('合成事件')}>点击</button>;
}
4. React 17 为什么移除事件池(Event Pooling)?
-
事件池作用:React 16 及之前,合成事件对象会被放入事件池复用(减少 GC 开销),事件处理完后属性会被清空,导致异步访问时获取不到值:
// React 16 问题:异步访问合成事件属性会丢失 const handleClick = (e) => { setTimeout(() => { console.log(e.target); // 输出 null(事件已被回收) }, 0); }; -
移除原因:
- 开发者频繁遇到异步访问问题,调试成本高;
- 现代浏览器 GC 效率提升,事件池的性能收益不明显;
- 简化合成事件系统设计,降低维护成本。
5. 合成事件如何模拟事件冒泡?与原生 DOM 冒泡有何区别?
-
合成事件冒泡模拟:基于 Fiber 树而非 DOM 树,通过「从目标 Fiber 向上遍历至根容器 Fiber」收集回调,再按相反顺序执行(模拟冒泡)。
-
与原生冒泡的区别:
- 范围不同:合成事件冒泡仅在 React 根容器内部的 Fiber 树中,原生事件冒泡到 document;
- 中断方式不同:合成事件用
e.stopPropagation(),原生事件用e.stopPropagation()或e.cancelBubble = true; - 目标不同:合成事件
e.target始终是触发事件的 DOM 节点,e.currentTarget是当前执行回调的组件对应的 DOM 节点(与原生一致)。
其他层级
1. React 18 自动批处理(Automatic Batching):原理与实现
核心结论:React 18 扩展了批处理范围,在 Promise、setTimeout 等异步回调中也会合并多次更新,核心通过 flushSync 控制强制刷新,底层依赖 Scheduler 的任务队列和 updateQueue 合并逻辑。
关键逻辑(伪代码映射源码)
// 1. 批处理开关:React 18 默认开启全局批处理
let isBatchingUpdates = true;
// 2. 触发更新的入口函数
function scheduleUpdateOnFiber(fiber, update) {
// 若处于批处理中,先入队不执行
if (isBatchingUpdates) {
enqueueUpdate(fiber, update); // 加入updateQueue队列
return;
}
// 非批处理状态,立即调度执行
flushSyncUpdates(fiber);
}
// 3. 异步场景的批处理包装(如Promise回调)
function wrapInBatch(callback) {
return () => {
const prevBatching = isBatchingUpdates;
isBatchingUpdates = true; // 进入批处理模式
try {
callback(); // 执行异步回调中的多次setState
} finally {
isBatchingUpdates = prevBatching;
// 回调执行完,若退出批处理,触发批量刷新
if (!prevBatching) {
flushPendingUpdates(); // 消费updateQueue,合并更新
}
}
};
}
// 4. flushSync:强制退出批处理,立即执行更新
function flushSync(callback) {
const prevBatching = isBatchingUpdates;
isBatchingUpdates = false; // 关闭批处理
try {
callback(); // 执行回调中的更新(立即触发)
} finally {
isBatchingUpdates = prevBatching;
}
}
2. React 合成事件系统:实现原理与事件委托
核心结论:React 不直接绑定原生事件,而是通过「事件委托」(委托到 document)和「合成事件对象」(SyntheticEvent)统一管理,底层依赖 Fiber 节点的 eventHandlers 存储事件回调。
关键逻辑(伪代码映射源码)
// 1. 合成事件对象(复用事件池,减少垃圾回收)
class SyntheticEvent {
constructor(nativeEvent) {
this.nativeEvent = nativeEvent;
this.target = nativeEvent.target;
// ... 其他属性(如bubbles、cancelable)
}
preventDefault() {
this.nativeEvent.preventDefault();
this.defaultPrevented = true;
}
// 事件池回收方法
release() {
SyntheticEvent.pool.push(this); // 放回池内复用
}
}
// 2. 事件委托初始化(挂载到document)
function initEventDelegation() {
['click', 'input', 'change'].forEach(eventType => {
document.addEventListener(eventType, handleNativeEvent);
});
}
// 3. 原生事件触发后的统一处理
function handleNativeEvent(nativeEvent) {
// 步骤1:从原生事件目标向上遍历Fiber树,收集事件回调
let currentFiber = getFiberFromDOM(nativeEvent.target);
const eventCallbacks = [];
while (currentFiber) {
const callback = currentFiber.memoizedProps[`on${nativeEvent.type}`];
if (callback) eventCallbacks.push(callback);
currentFiber = currentFiber.return; // 向上遍历父节点
}
// 步骤2:创建合成事件,执行回调
const syntheticEvent = SyntheticEvent.pool.pop() || new SyntheticEvent(nativeEvent);
eventCallbacks.reverse().forEach(callback => callback(syntheticEvent));
// 步骤3:回收合成事件到事件池
syntheticEvent.release();
}
// 4. Fiber节点存储事件回调
const fiberNode = {
memoizedProps: {
onClick: () => console.log('点击'), // 事件回调存储在props中
onInput: () => console.log('输入')
},
// ... 其他Fiber字段
};
3. React.memo、useMemo、useCallback:优化原理与区别
核心结论:三者均为性能优化手段,底层依赖「缓存对比」逻辑,区别在于优化对象不同(组件、计算结果、函数引用),伪代码揭示缓存存储与对比机制。
| 类型 | 生命周期 | 典型场景 |
|---|---|---|
| useEffect 内部变量 | 随回调执行创建,回调结束后可被回收(无引用时) | 仅在副作用中使用的临时变量 / 工具函数 |
| 函数组件内部变量 | 随每次渲染创建,渲染结束后可被回收 | 组件渲染所需的临时计算 |
关键逻辑(伪代码映射源码)
// 1. React.memo:缓存组件,基于props浅对比决定是否重渲染
function memo(Component, compare = shallowCompare) {
return function MemoizedComponent(props) {
const fiber = getCurrentFiber();
// 缓存上次的props
const prevProps = fiber.memoizedProps;
// 对比新旧props,相同则复用上次渲染结果
if (prevProps && compare(prevProps, props)) {
return fiber.memoizedState.element; // 复用缓存的元素
}
// 不同则重新渲染,更新缓存
const element = Component(props);
fiber.memoizedProps = props;
fiber.memoizedState.element = element;
return element;
};
}
// 2. useMemo:缓存计算结果,依赖变化时重新计算
function useMemo(create, deps) {
const fiber = getCurrentFiber();
const hook = getCurrentHook(); // 从memoizedState链表获取当前Hook
// 对比依赖数组,相同则复用缓存结果
if (hook.deps && shallowCompare(hook.deps, deps)) {
return hook.memoizedValue;
}
// 依赖变化,重新计算并更新缓存
const value = create();
hook.memoizedValue = value;
hook.deps = deps;
return value;
}
// 3. useCallback:缓存函数引用,依赖变化时重新创建
function useCallback(callback, deps) {
// 本质是useMemo的特例,缓存函数本身
return useMemo(() => callback, deps);
}
// 4. 浅对比工具函数(源码核心)
function shallowCompare(a, b) {
if (a === b) return true;
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
return false;
}
// 对比对象自身可枚举属性(浅对比)
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key) || a[key] !== b[key]) {
return false;
}
}
return true;
}
4. 错误边界(Error Boundary):实现原理与调用时机
核心结论:错误边界是类组件,通过生命周期方法 componentDidCatch 和 getDerivedStateFromError 捕获子组件渲染 / 副作用错误,底层依赖 Fiber 树的错误传播机制。
关键逻辑(伪代码映射源码)
// 1. 错误边界类组件(用户层面)
class ErrorBoundary extends React.Component {
state = { hasError: false };
// 静态方法:捕获错误后更新状态(渲染备用UI)
static getDerivedStateFromError(error) {
return { hasError: true };
}
// 实例方法:捕获错误并上报(副作用)
componentDidCatch(error, errorInfo) {
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) return <h1>出错了</h1>;
return this.props.children;
}
}
// 2. 底层错误传播逻辑(React源码层面)
function throwError(fiber, error) {
let currentFiber = fiber;
// 向上遍历Fiber树,寻找错误边界组件
while (currentFiber) {
// 检查当前Fiber是否为错误边界(类组件且实现了对应方法)
if (isErrorBoundary(currentFiber)) {
// 步骤1:触发getDerivedStateFromError,更新状态
const nextState = currentFiber.type.getDerivedStateFromError(error);
enqueueUpdate(currentFiber, {
action: () => nextState,
lane: SyncLane // 同步优先级,立即更新
});
// 步骤2:触发componentDidCatch,执行副作用
scheduleCallback(() => {
currentFiber.stateNode.componentDidCatch(error, {
componentStack: getComponentStack(currentFiber)
});
});
return; // 找到错误边界,终止传播
}
currentFiber = currentFiber.return; // 继续向上遍历
}
// 未找到错误边界,抛出到全局(控制台报错)
throw error;
}
// 3. 判断是否为错误边界
function isErrorBoundary(fiber) {
const Component = fiber.type;
return (
typeof Component === 'function' && // 类组件(函数类型)
(Component.prototype.getDerivedStateFromError || Component.prototype.componentDidCatch)
);
}
5. Suspense 实现原理:加载状态与数据请求协调
核心结论:Suspense 通过「抛出特殊 Promise」中断渲染,等待 Promise resolve 后恢复渲染,底层依赖 Fiber 的 deferedValue 和调度器的暂停 / 恢复机制。
关键逻辑(伪代码映射源码)
// 1. Suspense组件核心逻辑
function Suspense(props) {
const { fallback, children } = props;
const fiber = getCurrentFiber();
// 标记当前Fiber为Suspense节点,存储fallback
fiber.tag = SuspenseComponent;
fiber.memoizedProps.fallback = fallback;
return children;
}
// 2. 数据请求库适配(如React Query、SWR)
function fetchData(url) {
let status = 'pending';
let result;
const promise = fetch(url)
.then(res => res.json())
.then(data => {
status = 'resolved';
result = data;
});
// 关键:未就绪时抛出Promise,中断渲染
return () => {
if (status === 'pending') throw promise;
if (status === 'rejected') throw result;
return result;
};
}
// 3. 渲染时处理Suspense(Reconciliation阶段)
function performUnitOfWork(fiber) {
try {
// 正常处理子节点渲染
reconcileChildren(fiber);
} catch (error) {
// 捕获到Suspense抛出的Promise
if (error instanceof Promise) {
const suspenseFiber = findNearestSuspenseFiber(fiber); // 找到最近的Suspense父节点
// 步骤1:渲染fallback UI
suspenseFiber.memoizedState.isPending = true;
suspenseFiber.child = createFiberFromElement(suspenseFiber.memoizedProps.fallback);
// 步骤2:等待Promise resolve后,重新调度渲染
error.then(() => {
suspenseFiber.memoizedState.isPending = false;
scheduleUpdateOnFiber(suspenseFiber.return); // 重新触发渲染
});
} else {
throw error; // 其他错误交给错误边界处理
}
}
}
// 4. 查找最近的Suspense父节点
function findNearestSuspenseFiber(fiber) {
let current = fiber.return;
while (current) {
if (current.tag === SuspenseComponent) return current;
current = current.return;
}
return null;
}
6. React 18 useDeferredValue:低优先级状态延迟更新
核心结论:useDeferredValue 标记低优状态,延迟更新以避免阻塞高优任务,底层通过 Lane 模型分配低优先级 Lane,结合双缓存机制实现非阻塞渲染。
关键逻辑(伪代码映射源码)
function useDeferredValue(value, { timeoutMs } = {}) {
const fiber = getCurrentFiber();
const hook = getCurrentHook();
// 1. 初始化:缓存当前值和低优值
if (!hook.memoizedState) {
hook.memoizedState = {
currentValue: value,
deferredValue: value,
timeoutId: null
};
}
const state = hook.memoizedState;
const prevValue = state.currentValue;
// 2. 若值变化,更新当前值,并调度低优更新
if (value !== prevValue) {
state.currentValue = value;
// 分配低优先级Lane(如LowLane)
const lane = LowLane;
// 3. 取消之前的超时调度
if (state.timeoutId) clearTimeout(state.timeoutId);
// 4. 调度低优更新(延迟执行,或超时强制执行)
state.timeoutId = setTimeout(() => {
enqueueUpdate(fiber, {
action: () => {
state.deferredValue = value; // 更新低优值
return state;
},
lane
});
scheduleUpdateOnFiber(fiber);
}, timeoutMs || 1000);
}
// 5. 返回低优值(高优任务时返回旧值,不阻塞)
return state.deferredValue;
}
7. 高阶组件(HOC):定义、实现与设计原则
核心结论:HOC 是接收组件并返回新组件的函数,用于复用组件逻辑,底层依赖组件组合而非继承,需注意避免 props 覆盖、ref 丢失等问题。
关键逻辑(伪代码映射源码)
// 1. 基础HOC实现:增强组件props
function withExtraProps(WrappedComponent, extraProps) {
// 返回新组件(类组件或函数组件)
function HOCComponent(props) {
// 合并传入的props和额外props
return <WrappedComponent {...extraProps} {...props} />;
}
// 保留原始组件名称(便于调试)
HOCComponent.displayName = `withExtraProps(${getDisplayName(WrappedComponent)})`;
return HOCComponent;
}
// 2. 带状态的HOC:封装数据获取逻辑
function withFetch(WrappedComponent, url) {
return class FetchHOC extends React.Component {
state = { data: null, loading: true };
componentDidMount() {
fetch(url).then(res => res.json()).then(data => {
this.setState({ data, loading: false });
});
}
render() {
// 将状态作为props传递给被包装组件
return <WrappedComponent {...this.props} data={this.state.data} loading={this.state.loading} />;
}
};
}
// 3. 解决ref丢失问题(需用forwardRef)
function withRef(WrappedComponent) {
// 用forwardRef转发ref到被包装组件
return React.forwardRef((props, ref) => {
return <WrappedComponent {...props} ref={ref} />;
});
}
// 工具函数:获取组件名称(调试用)
function getDisplayName(Component) {
return Component.displayName || Component.name || 'Component';
}
// 4. 使用示例
const UserComponent = ({ data, loading }) => (
loading ? <div>加载中</div> : <div>{data.name}</div>
);
const UserWithFetch = withFetch(UserComponent, '/api/user'); // 增强后组件
核心注意事项:
- 避免 props 覆盖:HOC 传递的 props 若与外部传入的 props 重名,会覆盖外部 props(需规范命名或用命名空间);
- 不要在渲染时创建 HOC:每次渲染创建新组件会导致子组件重新挂载(销毁旧实例),引发性能问题;
- 组合优于嵌套:多层 HOC 嵌套会导致组件树复杂,可通过
compose函数扁平化(如compose(hoc1, hoc2)(Component))。
8. Context 实现原理:跨组件状态传递机制
核心结论:Context 通过Provider和Consumer实现跨层级状态传递,底层依赖 Fiber 树的contextDependencies记录依赖关系,状态更新时仅通知依赖组件。
关键逻辑(伪代码映射源码)
// 1. 创建Context(包含Provider和Consumer)
function createContext(defaultValue) {
// Context对象(存储当前值和订阅队列)
const context = {
_currentValue: defaultValue, // 当前值
_subscribers: new Set(), // 订阅该Context的组件
Provider: null,
Consumer: null
};
// 2. Provider组件:更新Context值并通知订阅者
context.Provider = class Provider extends React.Component {
constructor(props) {
super(props);
context._currentValue = props.value; // 初始化值
}
componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value) {
context._currentValue = this.props.value; // 更新值
// 通知所有订阅者重新渲染
context._subscribers.forEach(subscriber => {
subscriber.markNeedsUpdate(); // 标记组件需要更新
});
}
}
render() {
return this.props.children; // 渲染子树
}
};
// 3. Consumer组件:订阅Context并获取值
context.Consumer = function Consumer({ children }) {
const fiber = getCurrentFiber();
// 订阅Context(记录依赖关系)
context._subscribers.add(fiber);
// 组件卸载时取消订阅
fiber.cleanup = () => context._subscribers.delete(fiber);
// 执行children回调,传递当前值
return children(context._currentValue);
};
return context;
}
// 4. 使用示例
const ThemeContext = createContext('light');
// Provider提供值
function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
);
}
// Consumer消费值
function Child() {
return (
<ThemeContext.Consumer>
{theme => <div>当前主题:{theme}</div>}
</ThemeContext.Consumer>
);
}
核心原理:
- 依赖追踪:Consumer 渲染时,会将当前 Fiber 节点加入 Context 的
_subscribers集合,形成依赖关系; - 高效更新:Provider 值变化时,仅遍历
_subscribers集合,标记依赖组件更新,避免全树渲染; - 默认值生效时机:当组件在 Context Provider 外部使用时,才会使用
createContext的默认值(并非 Provider 未传入值时)。
9. ref 转发(forwardRef)与 useImperativeHandle:组件间通信
核心结论:forwardRef用于将父组件的 ref 转发到子组件内部元素,useImperativeHandle用于自定义暴露给父组件的实例方法,底层依赖 Fiber 的ref属性存储引用。
关键逻辑(伪代码映射源码)
// 1. forwardRef:转发ref到子组件
function forwardRef(render) {
const ForwardRefComponent = {
$$typeof: Symbol.for('react.forward_ref'), // 标记为forwardRef组件
render // 存储渲染函数(接收props和ref)
};
return ForwardRefComponent;
}
// 2. React处理forwardRef组件的逻辑(Reconciliation阶段)
function reconcileForwardRef(fiber, props, ref) {
// 调用forwardRef的render函数,传递props和ref
const element = fiber.type.render(props, ref);
// 创建子Fiber节点(指向内部元素)
fiber.child = createFiberFromElement(element);
}
// 3. useImperativeHandle:自定义暴露的ref方法
function useImperativeHandle(ref, createHandle, deps) {
const fiber = getCurrentFiber();
const hook = getCurrentHook();
// 初始化:创建handle并绑定到ref
if (!hook.memoizedState) {
hook.memoizedState = createHandle();
// 将handle赋值给ref(若ref存在)
if (ref) {
ref.current = hook.memoizedState;
}
}
// 依赖变化时更新handle
const prevDeps = hook.deps;
if (prevDeps && !shallowCompare(prevDeps, deps)) {
hook.memoizedState = createHandle();
if (ref) {
ref.current = hook.memoizedState;
}
hook.deps = deps;
}
}
// 4. 使用示例
const Input = forwardRef((props, ref) => {
const inputRef = useRef(null);
// 自定义暴露给父组件的方法
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
getValue: () => inputRef.current.value
}), []);
return <input ref={inputRef} {...props} />;
});
// 父组件使用
function Parent() {
const inputRef = useRef(null);
return (
<div>
<Input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>聚焦</button>
</div>
);
}
核心点:
- forwardRef 本质:并非组件,而是一个包含
render方法的对象,React 在协调阶段会调用该方法并传递ref; - useImperativeHandle 作用:限制父组件通过 ref 访问的内容(避免暴露整个 DOM 元素),只暴露必要方法,增强封装性。
10. 列表渲染中的 key:作用与原理, 列表渲染对比规则
核心结论:key 是 React 识别列表项身份的标识,用于优化 Diff 算法,减少不必要的 DOM 操作,底层通过 key 匹配复用 Fiber 节点。
关键逻辑(伪代码映射源码)
// React列表Diff核心逻辑(简化版)
function reconcileChildrenArray(prevChildren, nextChildren, fiber) {
const keyToIndexMap = new Map(); // 存储nextChildren的key到索引的映射
// 1. 构建nextChildren的key映射
nextChildren.forEach((child, index) => {
if (child.key != null) {
keyToIndexMap.set(child.key, index);
}
});
const result = [];
let prevIndex = 0;
// 2. 遍历prevChildren,寻找可复用节点
for (; prevIndex < prevChildren.length; prevIndex++) {
const prevChild = prevChildren[prevIndex];
let nextIndex = -1;
// 优先通过key匹配(精确匹配)
if (prevChild.key != null) {
nextIndex = keyToIndexMap.get(prevChild.key);
} else {
// 无key时,通过索引和类型匹配(容易出错)
for (let i = 0; i < nextChildren.length; i++) {
if (isSameType(prevChild, nextChildren[i])) {
nextIndex = i;
break;
}
}
}
if (nextIndex === -1) {
// 未找到匹配节点,标记为删除
prevChild.effectTag = Deletion;
} else {
// 找到匹配节点,复用并更新位置
const nextChild = nextChildren[nextIndex];
const reusedFiber = createFiberFromElement(nextChild);
reusedFiber.return = fiber;
reusedFiber.index = nextIndex;
result.push(reusedFiber);
keyToIndexMap.delete(prevChild.key); // 避免重复复用
}
}
// 3. 处理剩余的新节点(新增)
keyToIndexMap.forEach((index, key) => {
const nextChild = nextChildren[index];
const newFiber = createFiberFromElement(nextChild);
newFiber.return = fiber;
newFiber.index = index;
newFiber.effectTag = Placement; // 标记为新增
result.push(newFiber);
});
return result;
}
核心作用:
- 复用节点:key 相同且类型相同的节点会被复用(避免重新创建 DOM),只更新差异属性;
- 保持状态:列表项若有内部状态(如输入框内容),key 不变可保留状态(避免因重新创建节点丢失状态);
- 注意事项:不要用索引作为 key(列表增删排序时,key 会变化,导致节点复用错误),key 需在兄弟节点中唯一(无需全局唯一)。
11. useReducer 原理:与 useState 的关系及复杂状态管理
核心结论:useReducer 是 useState 的 “增强版”,通过 reducer 函数统一处理状态逻辑,底层与 useState 共享同一套更新队列机制(updateQueue),但支持更复杂的状态依赖逻辑。
关键逻辑(伪代码映射源码)
// 1. useReducer 与 useState 的关系:useState 本质是 useReducer 的特例
function useState(initialState) {
// 简化版:useState 内部调用 useReducer,reducer 直接返回新状态
return useReducer((state, action) => action, initialState);
}
// 2. useReducer 核心实现
function useReducer(reducer, initialState, init) {
const fiber = getCurrentFiber();
const hook = getCurrentHook();
// 初始化:处理懒初始化(init 函数)
if (!hook.memoizedState) {
const initialState = init ? init(initialState) : initialState;
hook.memoizedState = {
state: initialState, // 当前状态
queue: { pending: null } // 存储 update 队列(与 useState 共享结构)
};
}
const { state, queue } = hook.memoizedState;
// 3. dispatch 函数:创建 update 并加入队列
const dispatch = (action) => {
// 创建 update 对象(与 useState 的 update 结构一致)
const update = {
action,
next: null,
lane: SyncLane // 默认同步优先级
};
// 加入 updateQueue(循环链表)
enqueueUpdate(queue, update);
// 触发调度更新
scheduleUpdateOnFiber(fiber);
};
// 4. Reconciliation 阶段处理 update 队列
if (hasPendingUpdates(queue)) {
let newState = state;
let currentUpdate = queue.pending;
// 遍历队列,执行 reducer 计算新状态
do {
const action = currentUpdate.action;
newState = reducer(newState, action); // 核心:通过 reducer 处理状态
currentUpdate = currentUpdate.next;
} while (currentUpdate !== queue.pending);
// 清空队列,更新状态
queue.pending = null;
hook.memoizedState.state = newState;
}
return [state, dispatch];
}
核心:
- 与 useState 的区别:
useReducer适合状态逻辑复杂(多值依赖、多操作类型)的场景,useState适合简单状态; - 性能优化:当子组件需要通过回调更新父组件状态时,
useReducer的dispatch引用稳定(不会随渲染变化),可避免子组件不必要的重渲染(无需useCallback包裹)。
12. 服务器端渲染(SSR)与客户端水合(Hydration)原理
核心结论:SSR 是服务器生成 HTML 字符串发送给客户端,Hydration 是客户端将静态 HTML 激活为可交互 React 组件的过程,底层依赖 ReactDOMServer 与 ReactDOM.hydrateRoot 的协同。
关键逻辑(伪代码映射源码)
// 1. 服务器端渲染(ReactDOMServer.renderToString)
function renderToString(element) {
// 步骤1:创建服务器端 Fiber 树(无副作用,仅计算 DOM 结构)
const rootFiber = createFiberFromElement(element);
// 步骤2:协调阶段(Reconciliation)生成 DOM 字符串
let html = '';
function performUnitOfWork(fiber) {
if (fiber.tag === HostComponent) { // 原生 DOM 组件
html += `<${fiber.type}>`;
}
// 递归处理子节点
if (fiber.child) performUnitOfWork(fiber.child);
// 处理完子节点后关闭标签
if (fiber.tag === HostComponent) {
html += `</${fiber.type}>`;
}
}
performUnitOfWork(rootFiber);
return html; // 返回完整 HTML 字符串
}
// 2. 客户端水合(Hydration)过程
function hydrateRoot(container, element) {
// 步骤1:解析服务器生成的 HTML 结构,创建初始 DOM 映射
const domNodes = mapDOMNodes(container);
// 步骤2:创建客户端 Fiber 树,与 DOM 节点关联
const rootFiber = createFiberFromElement(element);
rootFiber.stateNode = container;
// 步骤3:执行水合协调(对比 Fiber 树与 DOM 结构,绑定事件/激活状态)
function hydrateUnitOfWork(fiber, domNode) {
fiber.stateNode = domNode; // 关联 Fiber 与 DOM 节点
// 绑定合成事件(激活交互能力)
if (fiber.memoizedProps.onClick) {
addEventListener(domNode, 'click', fiber.memoizedProps.onClick);
}
// 递归水合子节点
if (fiber.child) {
hydrateUnitOfWork(fiber.child, domNode.firstChild);
}
}
hydrateUnitOfWork(rootFiber, container.firstChild);
// 步骤4:标记水合完成,进入客户端渲染模式
rootFiber.mode = ConcurrentMode;
}
核心:
- SSR 优势:首屏加载快(HTML 直接渲染)、SEO 友好;
- Hydration 关键:需保证服务器与客户端生成的 DOM 结构一致(否则会导致水合失败,React 会暴力替换 DOM);
- 性能问题:水合是同步过程,大组件树可能阻塞主线程,React 18 引入
hydrateRoot支持并发水合(可中断)。
13. React DevTools Profiler:性能分析原理与优化实践
核心结论:Profiler 通过标记组件渲染时间、次数,帮助定位性能瓶颈,底层依赖 React 内部的性能测量钩子(mark/measure)和 Fiber 树的渲染追踪。
关键逻辑(伪代码映射源码)
// 1. Profiler 组件标记需要监控的区域
function Profiler({ id, onRender, children }) {
const fiber = getCurrentFiber();
// 标记当前 Fiber 为 Profiler 节点,记录 id 和回调
fiber.tag = ProfilerComponent;
fiber.memoizedProps = { id, onRender };
return children;
}
// 2. React 内部性能追踪逻辑
function performUnitOfWork(fiber) {
// 若当前节点在 Profiler 监控范围内,记录开始时间
if (isWithinProfiler(fiber)) {
const profiler = getNearestProfiler(fiber);
profiler.startTime = performance.now(); // 开始计时
}
// 执行正常协调逻辑(Diff、更新)
reconcileChildren(fiber);
// 记录结束时间,计算渲染耗时
if (isWithinProfiler(fiber)) {
const profiler = getNearestProfiler(fiber);
const duration = performance.now() - profiler.startTime;
// 触发 onRender 回调,传递性能数据
profiler.onRender(profiler.id, fiber.type, duration, ...);
}
}
// 3. 关键性能指标(Profiler 输出)
// - renderDuration:渲染耗时(协调阶段)
// - actualDuration:实际耗时(含子组件)
// - commits:提交次数(更新到 DOM 的次数)
核心考点:
- 优化方向:通过 Profiler 发现 “频繁重渲染” 组件,用
memo/useMemo优化;发现 “长任务渲染” 组件,用useTransition降级为低优更新; - 使用场景:复杂列表(如大数据表格)、动画场景、频繁交互组件(如输入框联想)。
14. 组件卸载后 setState 的问题与解决方案
核心结论:组件卸载后调用 setState 会导致内存泄漏警告,底层因 Fiber 节点已被标记为删除,但 updateQueue 仍有未处理更新,需在 useEffect 清理函数中取消订阅 / 定时器。
关键逻辑(伪代码映射源码)
// 问题场景:组件卸载后仍有异步操作触发 setState
function LeakyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// 异步请求,组件卸载后可能仍会执行
const timer = setTimeout(() => {
fetch('/api/data').then(res => setData(res.json())); // 卸载后调用setState
}, 1000);
// 未清理定时器,导致内存泄漏
// return () => clearTimeout(timer); // 缺失的清理逻辑
}, []);
return <div>{data?.name}</div>;
}
// React 检测到卸载后更新的警告逻辑
function scheduleUpdateOnFiber(fiber) {
// 检查 Fiber 是否已被标记为删除(卸载)
if (fiber.effectTag & Deletion) {
// 输出警告:"Can't perform a React state update on an unmounted component"
console.warn('Memory leak detected');
return; // 阻止执行更新
}
// 正常调度更新...
}
// 解决方案:使用清理函数取消异步操作
function SafeComponent() {
const [data, setData] = useState(null);
useEffect(() => {
let isMounted = true; // 标记组件是否挂载
const timer = setTimeout(() => {
fetch('/api/data').then(res => {
if (isMounted) setData(res.json()); // 仅在挂载时更新
});
}, 1000);
// 卸载时标记为未挂载
return () => {
clearTimeout(timer);
isMounted = false;
};
}, []);
return <div>{data?.name}</div>;
}