React 核心原理:渲染流程 · Fiber · Hook 链表 · Hooks
学习笔记整理版。全文围绕四个问题展开:一次更新走了哪些阶段、Fiber 与 Hook 链表长什么样、useState 怎么算出新 state、useEffect 什么时候执行。
目录
- 一、要掌握的核心知识点
- 二、整体渲染框架:开始阶段 → render → commit
- 三、Fiber 与 Hook 链表
- 四、Hooks 的调用时机:beginWork 阶段
- 五、useEffect 原理
- 六、总结与易错点
一、要掌握的核心知识点
JSX
组件
Props
State
Hooks
生命周期
Context
状态管理
事件机制
Fiber
Reconciliation
Diff
虚拟 DOM
性能优化
这些点可以按「写代码时用到什么」和「运行时怎么实现」分成四组:
| 分组 | 内容 | 一句话理解 |
|---|---|---|
| 视图与组件 | JSX、组件、Props、State | 用声明式的方式描述「界面应该长什么样」 |
| 交互与扩展 | Hooks、生命周期、事件机制、Context、状态管理 | 组件怎么响应交互、怎么共享数据 |
| 运行时机制 | 虚拟 DOM、Fiber、Reconciliation、Diff | React 怎么把「描述」变成真实的界面更新 |
| 性能优化 | memo、useMemo、useCallback、key | 让上面的机制少做无用功 |
后面几节就沿着「运行时机制」这条线走:先看整体流程,再看 Fiber 与 Hook 的数据结构,最后看 useState / useEffect 在流程里的具体位置。
二、整体渲染框架:开始阶段 → render → commit

2.1 四个阶段一览
| 阶段 | 能不能中断 | 主要工作 | 谁在这里工作 |
|---|---|---|---|
| ① 开始阶段(触发 + 调度) | — | setState 创建 Update、挂进队列、标记 lane、交给 Scheduler | Hook.queue(环形链表)、useState 的 dispatch |
| ② render 阶段 | 可以中断 / 丢弃重来 | 遍历 Fiber 树、执行组件函数、算出新 state、收集 effect | Fiber(beginWork / completeWork)、Hook 链表、useState、useEffect |
| ③ commit 阶段 | 不可中断,一口气跑完 | 真正操作 DOM、执行 useLayoutEffect、切换 current | Fiber(提交变更) |
| ④ 绘制之后 | 异步 | flush passive effects | useEffect 的 destroy / create |
记住一句话:render 阶段负责「算」,commit 阶段负责「改」,useEffect 负责「绘制之后再补作业」。
2.2 ① 开始阶段:setState 只是「登记」
调用 setCount(1) 时,React 并不会立刻重新渲染,而是:
- 找到这个组件对应的 Fiber 和它身上的
Hook.queue; - 创建一个
Update对象,把action(也就是c => c + 1)装进去; - 把 Update 挂到
queue.pending这个环形链表上; - 给 Fiber 标记 lane(更新优先级),再通过
scheduleUpdateOnFiber交给调度器。
这一阶段只碰「队列和优先级」,组件函数一次都还没执行,state 也还没重新计算。
2.3 ② render 阶段:只算,不动手
调度器决定开始工作后,进入 render 阶段:
workLoopSync/workLoopConcurrent不断取出下一个工作单元;performUnitOfWork拿到当前 Fiber,调用beginWork;beginWork向下:处理自己,并为子节点创建 / 复用 Fiber;completeWork向上:子节点都处理完,回到父节点收集信息。
函数组件在 beginWork 里会走到 updateFunctionComponent,再进入 renderWithHooks——所有 Hooks 都是在这时候被调用的。
这个阶段同时在做四件事:
| 角色 | 在 render 阶段做什么 |
|---|---|
| Fiber | 用 current ↔ workInProgress 双缓存,边遍历边构建新树 |
| Hook 链表 | 先置空新 Fiber 的链表,再沿旧链表顺序读、按同样顺序建新链表 |
| useState | 挂载走 mountState,更新走 updateState,执行 queue.pending 算出新 state |
| useEffect | 创建 effect 对象、浅比较 deps,只登记不执行 |
render 阶段随时可能被打断甚至丢弃重来,所以组件函数必须是纯的:不写 DOM、不发请求、不改全局变量。
2.4 ③ commit 阶段:动手改 DOM
render 阶段只产出「新的 workInProgress 树 + 收集好的 effect」,commit 阶段才真正落到页面上,而且一口气跑完、中途不会被打断:
- before mutation:DOM 变更前的准备,读取变更前的快照;
- mutation:真正插入 / 更新 / 删除 DOM 节点,执行
useLayoutEffect的 destroy; - layout:同步执行
useLayoutEffect的 create,处理 ref,最后把current指针切到新树上(相当于「换画」)。
2.5 ④ 绘制之后:useEffect 才真正执行
useEffect 的 create 不会在 layout 阶段同步执行,而是被调度到浏览器绘制完成之后,这样才不会阻塞渲染。它执行时的顺序是:
依赖变化 → 先执行上一次的 destroy → 再执行新的 create
组件卸载 → 执行最后一次 destroy
2.6 小结:hook、useState、useEffect、fiber 分别在哪个阶段
| 角色 | ① 开始阶段 | ② render 阶段 | ③ commit 阶段 | ④ 绘制之后 |
|---|---|---|---|---|
| Fiber | 被标记 lane,等待调度 | beginWork / completeWork 遍历,构建新 Fiber 树 |
切换 current、提交 DOM 变更 |
— |
| Hook 链表 | 更新挂进 queue.pending 环形链表 |
按顺序读旧链表、重建新链表 | — | — |
| useState | 创建 Update 对象入队(action) | mountState / updateState 算出新 state |
— | — |
| useEffect | — | 创建 effect、浅比较 deps(只登记) | useLayoutEffect 的 destroy / create 同步执行 |
useEffect 的 destroy → create 异步执行 |
三、Fiber 与 Hook 链表

3.1 一个 Counter 组件
function Counter() {
const [count, setCount] = useState(0); // Hook1
const [name, setName] = useState("react"); // Hook2
return (
<button onClick={() => setCount((c) => c + 1)}>
{count} {name}
</button>
);
}
3.2 首次渲染后:fiber.memoizedState 指向 Hook 链表
CounterFiber
memoizedState ──► Hook1
memoizedState: 0
queue: {
pending: null,
dispatch: setCount
}
next ──► Hook2
memoizedState: 'react'
queue: {
pending: null,
dispatch: setName
}
next: null
child ──► buttonFiber
return ──► AppFiber
也就是说:
fiber.memoizedState
-> hook1
-> hook2
-> null
Hook 的 next 把同一个组件里的多个 Hook 按调用顺序串起来。
所以 Hooks 不能写在条件语句里,因为更新时要靠顺序匹配。
⚠️ 注意区分两个同名属性:Fiber 的
memoizedState存的是「Hook 链表的头指针」,而每个 Hook 的memoizedState存的才是「这个 Hook 自己的状态」。
3.3 点击按钮:queue.pending 变成环形链表
调用 setCount(c => c + 1) 后:
Hook1.queue.pending ──► Update {
action: c => c + 1,
next: 自己
}
queue.pending 是一个环形链表:pending 永远指向最后一个 update,而最后一个 update 的 next 指回第一个。连续调用多次 setCount,这些 update 会按顺序挂在环形链表里,然后 React 调度更新,开始新一轮 render。
用环形链表的好处是入队 O(1):新 update 直接插在 pending 后面,而 pending.next 就是队首。
3.4 更新渲染时,Fiber 和 Hook 链表怎么配合
React 有双缓存:
current Fiber ──alternate──► workInProgress Fiber
- current Fiber:现在挂在墙上展示给观众看的画(当前页面)。
- workInProgress Fiber:你在后台桌子上正在画的新画(即将更新的页面)。
- alternate:墙上的画和桌子的画之间有一根绳子连着(互相指向对方)。当新画完成后,直接把它挂到墙上,旧的画拿下来备用。
更新时,React 会从 current.memoizedState 拿到旧 Hook 链表:
- current Fiber:就是墙上那幅旧画。
- memoizedState:在 Fiber 节点上,这个属性专门用来存放该组件的 Hooks 状态。但它存的不是一堆对象,而是链表的头节点(指针)。
- Hook1 -> Hook2 -> null:这代表你在组件里调用的 Hooks。比如:
const [count, setCount] = useState(0); // Hook1
const [name, setName] = useState('react'); // Hook2
currentFiber.memoizedState
-> Hook1(count=0)
-> Hook2(name='react')
-> null
🎭 四个核心「角色」
| 角色 | 身份 | 职责 |
|---|---|---|
workInProgress |
新画板 | 即将渲染的新 Fiber 节点 |
currentlyRenderingFiber |
全局广播 | 一个全局变量(指针),告诉整个 React 系统「现在正在加工哪个新画板」 |
currentHook |
旧账本导航员 | 一个全局变量,专门负责按顺序读取旧 Hook 链表 |
workInProgressHook |
新账本记录员 | 一个全局变量,专门负责按顺序记录新生成的 Hook 节点 |
3.5 简化内部结构
Fiber {
tag: FunctionComponent,
memoizedState: Hook | null, // 函数组件里:Hooks 链表头
child: Fiber | null, // 第一个子 Fiber
sibling: Fiber | null, // 下一个兄弟 Fiber
return: Fiber | null, // 父 Fiber
alternate: Fiber | null, // 双缓存
}
Hook {
memoizedState: any, // useState 的 state
queue: {
pending: Update | null, // 更新环形链表
dispatch: Function, // setState
},
next: Hook | null, // 下一个 Hook
}
Update {
action: any, // setCount(c => c + 1) 里的函数
next: Update | null, // 环形链表
}
3.6 为什么 Hook 不能写在条件语句里
- Hook 没有名字、也没有 ID,顺序就是身份:第 1 次
useState永远对应链表第 1 个 Hook; - 如果某次渲染因为
if少调用了一个 Hook,后面的 Hook 会整体错位; - 结果就是状态串位,甚至读到别的 Hook 的数据。
所以 useState / useEffect 必须写在组件函数顶层,循环、条件、嵌套函数里都不行。
四、Hooks 的调用时机:beginWork 阶段
4.1 调用链
React 的渲染流程会遍历 Fiber 树,对每个 Fiber 节点执行 beginWork(开始工作)。对于函数组件,beginWork 会根据 tag 属性进入 updateFunctionComponent 分支:
// 1. performUnitOfWork 开始处理一个 Fiber 节点
performUnitOfWork(unitOfWork);
// 2. 执行 beginWork 阶段
const next = beginWork(current, unitOfWork, renderLanes);
// 3. beginWork 内部根据 Fiber 的 tag 分发
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case FunctionComponent:
// 4. 函数组件进入 updateFunctionComponent
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
}
// 5. updateFunctionComponent 中调用 renderWithHooks
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes,
) {
// ... 准备上下文
// 6. 执行组件函数,并处理 Hooks
const nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
// ...
}
4.2 renderWithHooks 内部流程(准备开工)
renderWithHooks 内部主要做了四件事:
- 设置全局指针:将当前正在渲染的 Fiber 节点(
workInProgress)赋值给全局变量currentlyRenderingFiber。这样你在组件内调用useState时,React 才知道是哪个组件的状态。 - 选择 Hook 调度器:根据是首次挂载还是更新,切换
ReactCurrentDispatcher全局对象。挂载阶段使用HooksDispatcherOnMount(负责初始化),更新阶段使用HooksDispatcherOnUpdate(负责复用并计算新状态)。 - 执行组件函数:调用
Component(props, secondArg),也就是执行你写的函数组件。此时组件内的useState、useEffect等 Hooks 开始按顺序执行。 - 清理全局状态:组件函数执行完毕后,将
currentlyRenderingFiber等全局变量重置,并把 Hook 调度器恢复为ContextOnlyDispatcher(一个所有方法都会报错的调度器),防止在组件外部错误调用 Hooks。
对应的源码骨架:
renderWithHooks 被调用来执行你的组件函数:
function renderWithHooks(current, workInProgress, Component, props) {
// 1. 广播:告诉系统,现在正在加工的「新画板」是它
currentlyRenderingFiber = workInProgress;
// 2. 清空新画板:把新 Fiber 上的 Hook 链表置空,准备全新构建
workInProgress.memoizedState = null;
// 3. 记录员就位:新链表还没有节点,指向 null
workInProgressHook = null;
// 4. 导航员就位:如果存在旧 Fiber,就指向旧的 Hook 链表头;如果是首次挂载,则为 null
currentHook = current ? current.memoizedState : null;
// 5. 开始执行组件函数(极其关键!)
// 此时会执行你的 Counter 组件代码,里边的 useState 会被依次调用
const children = Component(props);
// 6. 收尾清理:全局变量置空,防止影响下一个组件的渲染
currentlyRenderingFiber = null;
return children;
}
4.3 调用 useState 时的「流水线」细节
当 Component() 执行、遇到第一个 useState 时,React 内部发生了以下精妙的配合。
🔹 调用第一个 useState(count)
- 从
currentHook拿到旧 Hook1:导航员(currentHook)从旧链表头上摘下了第一颗果实(Hook1,里面记录着旧的count = 0)。 - 处理
Hook1.queue.pending里的 update:检查 Hook1 身上的「待办事项」(更新队列)。发现有人调用了setCount(1),所以队列里有个 update。 - 算出新 state:
count = 1:将队列里的 update 依次执行,计算出最新的状态 1。 - 创建新的
workInProgressHook,挂到workInProgress.memoizedState:记录员(workInProgressHook)在「新画板」上创建一个全新的 Hook 节点(记录为count = 1)。因为这是第一个 Hook,所以workInProgress.memoizedState直接指向它,新链表有了头节点。
🔹 调用第二个 useState(name)
- 从
currentHook.next拿到旧 Hook2:导航员顺着旧链表的next指针,拿到了第二个旧 Hook(记录着name = 'react')。 - 复用 state:
name = 'react':检查 Hook2 的更新队列,发现没有setName的调用,于是直接复用旧状态。 - 记录员新建一个新 Hook,写入
name = 'react',并把它挂到上一个新 Hook 的next上。(此时新链表:Hook1(count=1) -> Hook2(name='react') -> null)
如果还有第三个 useState,导航员就继续 currentHook.next.next,记录员继续往后接新节点,如此往复,直到组件函数执行完毕。
补充:更新时新 Hook 会复用旧 Hook 的
queue,所以dispatch(如setCount)的函数引用是稳定的,多次更新指向的仍然是同一个 Hook。
4.4 相关核心函数
与 renderWithHooks 紧密协作的核心函数包括:
| 函数 | 作用 |
|---|---|
beginWork |
Fiber 处理的入口,负责根据 tag 将工作分发给不同的处理函数 |
updateFunctionComponent |
beginWork 处理函数组件的分支,是 renderWithHooks 的直接调用者 |
reconcileChildren |
renderWithHooks 返回的 ReactElement 会作为参数传入,用于协调(diff)子节点,生成新的 Fiber 树 |
HooksDispatcherOnMount / HooksDispatcherOnUpdate |
两个 Hook 调度器对象,renderWithHooks 根据渲染阶段切换它们,决定了 useState 等 Hooks 的实际行为 |
mountState / updateState |
useState 在挂载和更新阶段分别调用的底层函数,负责处理状态初始化或更新队列 |
五、useEffect 原理

5.1 存储位置:Fiber 的 memoizedState 链表
函数组件对应一个 Fiber 节点。Fiber 上有个 memoizedState 属性,指向一个 Hook 链表。每个 Hook 节点大概长这样:
{
memoizedState: null, // 对于 useEffect,存的是 effect 对象
baseState: null,
baseQueue: null,
queue: null,
next: null // 指向下一个 Hook
}
调用 useEffect 时,React 会创建一个 effect 对象:
{
tag: HookEffectTag,
create: () => { /* 副作用函数 */ },
destroy: undefined, // 清理函数
deps: [dep1, dep2],
next: null
}
这个 effect 对象会挂到当前 Hook 节点的 memoizedState 上,并且所有 effect 还会通过 next 串成一个 effect 链表,存在 Fiber 的 updateQueue 上。
关键点:Hook 链表用来保存状态本身,effect 链表用来保存「这一轮要执行的副作用」。
5.2 render 阶段:创建 effect,对比依赖
每次组件渲染,执行到 useEffect 时:
- 如果是首次渲染,创建 Hook 节点和 effect 对象;
- 如果是更新,会从当前 Hook 链表取出对应的 Hook,然后浅比较新旧 deps:
| deps 写法 | 行为 |
|---|---|
| 不传 deps | 每次 render 后都要执行 |
deps 是空数组 [] |
只在挂载后执行一次,卸载时清理 |
deps 有值 [deps] |
依赖项有变化才执行 |
如果依赖没变,React 不会给这个 effect 打上 HookHasEffect 标记,commit 阶段就会跳过它。
源码补充:并没有一个叫
NoHookEffect的标记,准确说法是「依赖没变 → 不设置HookHasEffect」,commit 阶段据此过滤。
5.3 commit 阶段:收集完之后才执行
render 阶段只是收集 effect,真正执行在 commit 阶段。
commit 分三个子阶段:
- before mutation:变更前的准备;
- mutation:操作真实 DOM;
- layout:执行
useLayoutEffect。
useEffect 的 create 函数不会在 layout 阶段同步执行,而是被调度到浏览器绘制之后异步执行。所以它不会阻塞浏览器渲染。
5.4 清理函数
useEffect 的 create 可以返回一个函数,作为清理函数,保存在 effect 的 destroy 上。
- 组件卸载时,执行
destroy; - 依赖变化导致下次 effect 执行前,先执行上一次的
destroy,再执行新的create。
5.5 和 useLayoutEffect 的区别
| useEffect | useLayoutEffect | |
|---|---|---|
| 执行时机 | 浏览器绘制之后(异步) | DOM 变更后、绘制之前(同步,layout 阶段) |
| 会不会阻塞渲染 | 不会 | 会 |
| 常见用途 | 请求数据、订阅、日志 | 需要同步读取 / 修改 DOM 布局的场景 |
六、总结与易错点
- Hooks 的顺序就是身份:Fiber 上没有 Hook 的名字或 ID,更新时靠「调用顺序 ↔ 链表顺序」一一对应,所以
useState/useEffect不能写在条件、循环或嵌套函数里。 - 两个
memoizedState不是一回事:Fiber 的memoizedState是 Hook 链表的头指针,Hook 的memoizedState才是这个 Hook 自己的状态。 - setState 是先入队、后计算:调用时只是把 Update 挂进
queue.pending环形链表并标记优先级,真正算新 state 是在下一轮 render 的updateState里。 - 连续 setState 用函数式更新更安全:
setCount(c => c + 1)会依次执行队列里的每个 update;写成setCount(count + 1)则可能拿到闭包里的旧值。 - render 阶段必须保持纯:它可能被打断、丢弃、重来,所以不要在组件函数体里写 DOM、发请求或改全局变量。
- render 负责算,commit 负责改,effect 负责补:
useEffect在绘制之后异步执行;需要同步读取 / 修改 DOM 就用useLayoutEffect。 - 双缓存的意义:同时保留
current和workInProgress两棵树,新树算好之前旧树一直挂在页面上,用户看不到半成品。