React 深度指南:从 Fiber 到 Hooks,万字长文带你吃透原理
上周有个读者私信我:"React 用了两年,setState 都会写,但面试官问 Fiber 是什么,脑子一片空白。" 这不是他一个人的问题。
写在前面
面试的时候,你有没有遇到过这种情况:
- 问 React 生命周期,能答个七七八八
- 问 Hooks 原理,只能说"闭包+链表"
- 问 Fiber 架构,直接懵了
我花了两周时间,把 React 从哲学到源码梳理了一遍,做成了一个结构化的知识图谱。今天把核心内容分享出来,万字长文,建议收藏。
GitHub 项目地址:react-deep-dive
一、React 的核心哲学:声明式 UI
命令式 vs 声明式
jQuery 时代(命令式):
// 告诉浏览器"怎么做"
$('#list').empty();
data.forEach(item => {
$('#list').append(`<li>${item.name}</li>`);
});
React 时代(声明式):
// 告诉 React"想要什么"
function List({ data }) {
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
核心区别:
| 维度 | 命令式 | 声明式 |
|---|---|---|
| 思维方式 | 过程导向 | 结果导向 |
| 代码量 | 多 | 少 |
| 维护性 | 难 | 易 |
| Bug 率 | 高 | 低 |
React 的哲学可以用一句话概括:
UI = f(State)
给定相同的 State,永远得到相同的 UI。
二、Virtual DOM:不是为了快,而是为了声明式
很多人以为 Virtual DOM 是为了性能,其实不是。
Virtual DOM 解决了什么问题?
- 跨平台:一套代码可以跑 Web、Native、VR
- 声明式:开发者只需要描述 UI,React 负责更新
- 可中断:为并发模式打下基础
Diff 算法的三个假设
React 的 diff 算法基于三个假设:
- 只比较同层:不跨层级移动
- 类型不同直接替换:
div变span就销毁重建 - Key 标识:通过 key 识别哪些节点是相同的
这三个假设让 diff 的时间复杂度从 O(n³) 降到 O(n)。
Diff 过程:
新树节点 vs 旧树节点
↓
类型相同? → 是 → 更新属性,递归比较 children
↓ 否
直接替换(销毁旧的,创建新的)
"理解 Diff 算法,是理解 Fiber 的前提。"
三、Fiber 架构:React 的灵魂
这是面试高频考点,也是理解 React 的关键。
为什么需要 Fiber?
React 15 用递归遍历 Virtual DOM 树,问题是不可中断:
// React 15:递归,一旦开始无法停止
function reconcile(node) {
updateDOM(node);
node.children.forEach(child => {
reconcile(child); // 卡住了!
});
}
如果树很大,主线程被占用几百毫秒,用户点击、输入就会卡顿。
问题场景:
用户点击按钮
↓
React 开始渲染 1000 个列表项
↓
主线程被占用 500ms
↓
用户的点击/输入完全无响应 → 页面卡顿!
Fiber 的解决方案
Fiber 把递归变成了可中断的循环:
// React 16+:链表遍历,可以中断
function workLoop(deadline) {
while (nextUnitOfWork && deadline.timeRemaining() > 1) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork) {
// 还有工作,让出主线程,等下一帧继续
requestIdleCallback(workLoop);
} else {
// 渲染完成,提交到 DOM
commitRoot();
}
}
优化后:
用户点击按钮
↓
React 开始渲染列表
↓
渲染了 100 个,时间用完了
↓
让出主线程,处理用户输入 ✓
↓
下一帧继续渲染剩下的 900 个
↓
用户交互流畅,列表也渲染完了
一句话总结
Fiber 就是把递归 diff 变成了可中断的链表遍历。
Fiber 节点结构
每个 React 元素对应一个 Fiber 节点:
interface Fiber {
type: string | Function; // 'div' 或组件函数
child: Fiber | null; // 第一个子节点
sibling: Fiber | null; // 下一个兄弟节点
return: Fiber | null; // 父节点
pendingProps: any; // 新 props
memoizedState: any; // 上次渲染的 state
flags: number; // 标记需要执行的操作
alternate: Fiber | null; // 双缓冲指针
}
三种指针的遍历顺序:
parent
↓ child
child → sibling → sibling → null
↓ child
grandchild → sibling → null
双缓冲机制
React 维护两棵 Fiber 树:
- current tree:当前屏幕显示的
- workInProgress tree:正在内存中构建的
current tree (屏幕上显示的)
↕ alternate 指针
workInProgress tree (正在构建的)
↓ 构建完成后,原子切换
新的 current tree
构建完成后,通过一个原子操作将其变为 current 树。好处是构建过程中如果被中断,current 树不受影响。
四、Hooks 原理:闭包 + 链表
"Fiber 解决了'能不能中断'的问题,那 Hooks 解决了'逻辑能不能复用'的问题。"
为什么 Hooks 不能在条件语句中调用?
Hooks 是按调用顺序存储在链表中的:
// 第一次渲染
useState('A'); // hooks[0]
useState('B'); // hooks[1]
// 第二次渲染(条件变化)
useState('A'); // hooks[0] ✓
// useState('B') 被跳过了
useState('C'); // hooks[1] ✗ 错误!应该是 hooks[2]
useState 的简化实现
let hooks: any[] = [];
let hookIndex = 0;
function useState<T>(initialValue: T): [T, (newValue: T) => void] {
const idx = hookIndex;
hooks[idx] = hooks[idx] ?? initialValue;
function setState(newValue: T) {
hooks[idx] = newValue;
render(); // 触发重新渲染
}
hookIndex++;
return [hooks[idx], setState];
}
闭包陷阱
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
alert(`当前值: ${count}`); // 永远是 0!
}, 3000);
}
return <button onClick={handleClick}>3秒后弹出</button>;
}
原因: 闭包捕获的是渲染时的值,不是最新值。
解决方案: 用 useRef 或函数式更新。
// 方案1:函数式更新
setCount(prev => {
alert(`当前值: ${prev}`);
return prev;
});
// 方案2:useRef
const countRef = useRef(count);
countRef.current = count;
setTimeout(() => alert(countRef.current), 3000);
五、性能优化:什么时候该用 memo?
React.memo
// 只有 props 变化时才重新渲染
const Child = React.memo(function Child({ data }) {
return <div>{data}</div>;
});
useMemo / useCallback
// 缓存计算结果
const value = useMemo(() => computeExpensive(a, b), [a, b]);
// 缓存函数引用
const handleClick = useCallback(() => doSomething(), []);
什么时候该用?
| 场景 | 使用 |
|---|---|
| 昂贵的计算 | useMemo |
| 传给 memo 组件的回调 | useCallback |
| 普通计算 | 不用 |
| 普通函数 | 不用 |
真实案例:列表渲染优化
// 优化前:1000 条数据,每次输入都重新渲染
function App() {
const [input, setInput] = useState('')
const [list] = useState(generateLargeList()) // 1000 条
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} />
{list.map((item, i) => (
<ExpensiveItem key={i} data={item} />
))}
</div>
)
}
// 每次输入,1000 个 ExpensiveItem 全部重新渲染 → 卡顿
// 优化后:只渲染变化的部分
const ExpensiveItem = React.memo(({ data }) => (
<div>{data.name}</div>
))
function App() {
const [input, setInput] = useState('')
const [list] = useState(generateLargeList())
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} />
{list.map((item, i) => (
<ExpensiveItem key={i} data={item} />
))}
</div>
)
}
// 输入时,ExpensiveItem 的 props 没变,跳过渲染 → 流畅
优化效果:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 输入响应延迟 | 200ms | 5ms |
| 渲染耗时 | 150ms | 10ms |
| 重新渲染组件数 | 1000 个 | 1 个 |
记住:过早优化是万恶之源。先测量,再优化。
六、React 19 新特性
React 19 是当前的流量密码,也是面试的新热点。
1. Actions:简化异步状态更新
React 18 的痛点:
// React 18:需要手动管理 loading 状态
const [isPending, setIsPending] = useState(false)
async function handleSubmit() {
setIsPending(true)
try {
await updateName(name)
} finally {
setIsPending(false)
}
}
React 19 的改进:
// React 19:Actions 自动管理
function UpdateName() {
const [name, setName] = useState('')
const [isPending, startTransition] = useTransition()
async function handleSubmit() {
startTransition(async () => {
await updateName(name)
})
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button disabled={isPending}>
{isPending ? '更新中...' : '更新'}
</button>
</form>
)
}
解决的问题:
- 不再需要手动管理 loading 状态
- 异步操作和状态更新统一处理
- 代码更简洁,不容易出错
2. useOptimistic:乐观更新
什么是乐观更新?
用户点击"点赞",先立即显示已点赞状态(乐观),后台再发请求。如果请求失败再回滚。
实际场景:
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, sending: true }]
)
async function handleAdd(formData) {
const newTodo = { text: formData.get('text') }
// 先乐观更新,让用户立即看到结果
addOptimisticTodo(newTodo)
// 再发真实请求
await addTodo(newTodo)
}
return (
<form action={handleAdd}>
{optimisticTodos.map(todo => (
<div key={todo.id} style={{ opacity: todo.sending ? 0.5 : 1 }}>
{todo.text}
{todo.sending && ' (发送中...)'}
</div>
))}
</form>
)
}
使用场景:
- 点赞/收藏按钮
- 评论发送
- 待办事项添加
- 任何需要"先显示结果,后确认"的场景
3. React Compiler:告别 useMemo
现在的痛点:
// 开发者需要手动写一堆优化代码
const MemoizedChild = React.memo(Child)
const memoizedValue = useMemo(() => compute(a, b), [a, b])
const memoizedCallback = useCallback(() => doSomething(), [])
React Compiler 的愿景:
// 以后可能只需要这样写
function App({ a, b }) {
const value = compute(a, b)
const handleClick = () => doSomething()
return <Child value={value} onClick={handleClick} />
}
// 编译器自动分析依赖,自动添加记忆化
这意味着什么?
- 不再需要手动写 useMemo/useCallback
- 代码更简洁,心智负担更低
- 性能优化由编译器自动完成
面试高频题
Q1: 为什么 React 16 要重写渲染引擎?
递归渲染不可中断,无法优先级调度。Fiber 通过链表结构实现了可中断和优先级。
Q2: useEffect 和 useLayoutEffect 的区别?
useEffect 在浏览器绘制后执行,useLayoutEffect 在 DOM 更新后、绘制前执行。
Q3: React.memo 和 useMemo 的区别?
React.memo 避免组件重渲染,useMemo 缓存计算结果。
Q4: 什么是闭包陷阱?如何解决?
闭包捕获旧值的问题。用 useRef 或函数式更新解决。
一起参与
这篇文章只覆盖了 React 知识图谱的一部分。完整项目包含 7 个章节、50+ 图解、30+ 面试题, 全部开源在 GitHub:
下一步计划:
- 状态管理全景(Context vs Zustand vs Redux)
- React 19 完全解读
- 大厂面试 100 题
如果你有想补充的内容,欢迎提 PR 或 Issue 一起完善。 觉得有帮助的话,点个 ⭐ 就是最大的支持 🙏
延伸阅读: