深色模式
Fiber 架构与源码思路
29. 为什么 React 需要 Fiber?
展开答案与追问
参考答案: 传统递归协调把大量工作压在连续调用栈中,不便暂停后恢复。Fiber 将组件树工作显式表示为可遍历的工作单元,支持按优先级安排、暂停、恢复或丢弃尚未提交的结果。目标是改善复杂更新中的响应性,而非保证每次总耗时更短。
追问与易错点: 来源:Fiber 数据结构。Fiber 不是浏览器线程,也不是真实 DOM。
30. Fiber 节点常见字段有哪些?
展开答案与追问
参考答案: type/tag 表示节点类型,key 表示身份;child、sibling、return 描述子节点、兄弟和父级关系;pendingProps/memoizedProps 保存待处理及已记忆属性,memoizedState 保存状态;alternate 连接对应版本,flags/subtreeFlags 记录工作标记,lanes/childLanes 关联更新优先级。stateNode 的含义随节点类型而异。
追问与易错点: 这是源码阅读的概念导航,字段属于内部实现,不能作为业务 API;函数组件的 stateNode 不等于类实例。
31. current 与 workInProgress 的双缓冲如何理解?
展开答案与追问
参考答案: current 描述当前已提交树,workInProgress 用来计算候选结果,通常通过 alternate 对应。完成后提交候选树并切换当前引用;未完成候选可以重做或放弃。React 会复用节点与共享结构,不是每次把整棵 DOM 深拷贝两份。
追问与易错点: 可用“当前展示版本 / 正在计算版本”口述;双缓冲并不意味着页面里同时存在两套完整真实 DOM。
32. render 与 commit 两个阶段有什么区别?
展开答案与追问
参考答案: render 计算下一棵树及其变更,可在并发工作中中断、重试或丢弃;不能随意产生可观察副作用。commit 将完成结果作用到宿主环境,核心 DOM mutation 与 layout 工作同步完成,通常不在中途让出执行权。passive Effects 的处理另有调度时序。
追问与易错点: 用户可见输出应来自完整提交,不能把 render 的中间结果直接暴露到 DOM。Render 与 Commit
33. beginWork、completeWork 与工作循环怎么理解?
展开答案与追问
参考答案: 概念上,beginWork 处理当前节点并决定向哪个子节点继续;子树处理完后 completeWork 完成当前节点相关工作,再找兄弟或返回父级。通过显式指针遍历,React 可以在工作单元之间检查是否需要让出。
追问与易错点: 来源:工作循环。不是每执行一行组件代码都能中断;单个耗时函数仍可能占住主线程。
34. Scheduler、Lane 与 Reconciler 如何分工?
展开答案与追问
参考答案: Reconciler 决定组件树应如何更新;Lane 用位集合描述更新类别与待处理工作,帮助选取、合并或跳过更新;Scheduler 为可调度工作安排执行机会与优先级。Lane 与 Scheduler 优先级有关联,但不是一套完全相同的枚举。
追问与易错点: 来源:Lane 实现。不要死背位数或固定毫秒切片,细节会随版本变化。
35. 低优先级更新被跳过,会丢失吗?
展开答案与追问
参考答案: React 会保留必要的更新队列信息,在后续合适的优先级处理中重放或基于正确状态重新计算,保持更新顺序语义。函数式 updater 应纯净,因为计算可能被重试。一次被丢弃的是候选渲染结果,不代表业务更新必然丢弃。
追问与易错点: 示意:紧急输入可以先提交,昂贵列表稍后完成;不要把队列算法简化成只保留最后一个赋值。
36. Hooks 为什么依赖调用顺序?内部大致如何保存?
展开答案与追问
参考答案: 在函数组件 Fiber 中,常规 Hook 状态按调用顺序关联为内部结构;渲染时依序读取相应状态与队列。条件分支改变调用数量或顺序,会让之后的状态关联错位。useState 的 setter 将更新加入队列,随后在对应渲染中计算。
追问与易错点: 这是常规状态 Hook 的概念模型,不要声称所有 use 开头 API 都创建同一种链表节点。
37. React Diff 为什么不求任意树的最小编辑距离?
展开答案与追问
参考答案: 通用树最优编辑代价高,React 通过类型、同级位置与 key 等启发式规则判断复用与替换。不同类型通常重建,同级稳定 key 支持识别移动。它追求可预测的工程成本,不保证全局最少 DOM 操作。
追问与易错点: React 列表协调不等于 Vue 3 的最长递增子序列方案;key 也不能让组件跨任意父级自动保留状态。
38. 如何在 90 秒内完整回答 Fiber?
展开答案与追问
参考答案: 先说痛点:长任务影响交互;再说结构:Fiber 显式记录工作单元与树关系;接着说过程:current/workInProgress 计算候选树,render 可中断,commit 完成宿主变更;最后说调度:Lane 表达优先级,Scheduler 安排执行机会,并发不是多线程。
追问与易错点: 加分边界:不能中断单个正在执行的长函数;不能在 render 做副作用;底层实现不属于稳定公开 API。
速查表与代码示例
常见 API 速查
| API | 主要用途 | 面试易错点 |
|---|---|---|
| useState / useReducer | 保存状态 / 集中状态转换 | setter 不修改当前渲染快照;reducer 要纯净 |
| useContext | 消费跨层依赖 | memo 不屏蔽所消费 Context 的变化 |
| useRef | DOM 与非展示性可变值 | 改 current 不会自动重渲染 |
| useEffect | 外部系统同步 | 清理与依赖完整,不能保证总在绘制后 |
| useLayoutEffect | 绘制前测量与校正 | 会阻塞绘制,避免滥用 |
| useMemo / useCallback | 缓存计算 / 函数引用 | 依赖错误会产生旧值,不作为正确性机制 |
| memo | 基于 props 比较跳过部分渲染 | 自身状态与 Context 仍能触发更新 |
| useTransition | 非紧急更新与 pending | 不会自动取消网络请求 |
| useDeferredValue | 延后某个值的消费 | 不是固定延迟防抖 |
| useId | 可访问性关联 ID | 不用作列表业务 key |
| useImperativeHandle | 控制对外 ref 句柄 | 优先声明式数据流 |
| useSyncExternalStore | 外部 store 一致订阅 | 快照需满足稳定性约定 |
类生命周期与函数组件思路对照
| 需求 | 类组件常见位置 | 函数组件思路 |
|---|---|---|
| 初始化状态 | constructor / state 字段 | useState 惰性初始化或 useReducer |
| 根据输入计算 UI | render | 函数组件主体,保持纯净 |
| 订阅外部系统并随输入重连 | didMount + didUpdate | 一个 Effect 描述设置、依赖与清理 |
| DOM 提交后、绘制前测量 | didMount/didUpdate 中相关逻辑 | useLayoutEffect |
| DOM 变更前保留快照 | getSnapshotBeforeUpdate | 没有可机械替换的单一 Hook,需重新设计读取时机 |
| 释放订阅 | componentWillUnmount | 对应 Effect 返回清理函数 |
| 捕获后代渲染错误 | getDerivedStateFromError / componentDidCatch | 仍可用类 Error Boundary 或封装库 |
不要把整个生命周期表理解为“一行对应一个 Hook”。Effect 是围绕一个外部同步过程组织,而非复刻类组件执行位置。
示例:请求依赖变化与清理
jsx
import { useEffect, useState } from 'react';
function UserPanel({ userId }) {
const [result, setResult] = useState({ status: 'loading' });
useEffect(() => {
const controller = new AbortController();
let active = true;
setResult({ status: 'loading' });
async function load() {
try {
const response = await fetch(`/api/users/${encodeURIComponent(userId)}`, {
signal: controller.signal,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (active) setResult({ status: 'success', data });
} catch (error) {
if (active) setResult({ status: 'error', message: String(error) });
}
}
load();
return () => {
active = false;
controller.abort();
};
}, [userId]);
if (result.status === 'loading') return <p>加载中</p>;
if (result.status === 'error') return <p role="alert">{result.message}</p>;
return <p>{result.data.name}</p>;
}讲解重点:Effect 跟随 userId 同步;清理同时使旧回调失效并尝试取消网络;所有请求结果统一按身份写入。这个简化示例不含缓存、SSR 数据预取和严格的“参数刚变化即清除旧显示”策略,真实路由页面可优先采用框架的数据加载层。
Fiber 概念图
这是概念图,省略了挂起、错误恢复、更新队列重放和多 root 等路径。常见内部源码入口:Fiber、工作循环、Lane。链接指向会变化的 main 分支,阅读具体项目时应切换到对应 tag。
建议复习顺序
- 第一轮:1~20 题,能讲清状态、API、生命周期与副作用。
- 第二轮:21~28 题,结合实际组件解释身份、并发与性能。
- 第三轮:29~38 题,用 90 秒串起 Fiber,并能回答边界。