resin-blog
首页归档简历规划关于
...
赣ICP备2026011201号-1

React 核心原理:渲染流程 · Fiber · Hook 链表 · Hooks

2026-09-17· React

学习笔记整理版。全文围绕四个问题展开:一次更新走了哪些阶段、Fiber 与 Hook 链表长什么样、useState 怎么算出新 state、useEffect 什么时候执行。

目录

  • 一、要掌握的核心知识点
  • 二、整体渲染框架:开始阶段 → render → commit
  • 三、Fiber 与 Hook 链表
  • 四、Hooks 的调用时机:beginWork 阶段
  • 五、useEffect 原理
  • 六、总结与易错点

一、要掌握的核心知识点

JSX
组件
Props
State
Hooks
生命周期
Context
状态管理
事件机制
Fiber
Reconciliation
Diff
虚拟 DOM
性能优化

React 核心知识框架总览 这些点可以按「写代码时用到什么」和「运行时怎么实现」分成四组:

分组 内容 一句话理解
视图与组件 JSX、组件、Props、State 用声明式的方式描述「界面应该长什么样」
交互与扩展 Hooks、生命周期、事件机制、Context、状态管理 组件怎么响应交互、怎么共享数据
运行时机制 虚拟 DOM、Fiber、Reconciliation、Diff React 怎么把「描述」变成真实的界面更新
性能优化 memo、useMemo、useCallback、key 让上面的机制少做无用功

后面几节就沿着「运行时机制」这条线走:先看整体流程,再看 Fiber 与 Hook 的数据结构,最后看 useState / useEffect 在流程里的具体位置。


二、整体渲染框架:开始阶段 → render → commit

React 一次更新的完整阶段图

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 并不会立刻重新渲染,而是:

  1. 找到这个组件对应的 Fiber 和它身上的 Hook.queue;
  2. 创建一个 Update 对象,把 action(也就是 c => c + 1)装进去;
  3. 把 Update 挂到 queue.pending 这个环形链表上;
  4. 给 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 链表

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 内部主要做了四件事:

  1. 设置全局指针:将当前正在渲染的 Fiber 节点(workInProgress)赋值给全局变量 currentlyRenderingFiber。这样你在组件内调用 useState 时,React 才知道是哪个组件的状态。
  2. 选择 Hook 调度器:根据是首次挂载还是更新,切换 ReactCurrentDispatcher 全局对象。挂载阶段使用 HooksDispatcherOnMount(负责初始化),更新阶段使用 HooksDispatcherOnUpdate(负责复用并计算新状态)。
  3. 执行组件函数:调用 Component(props, secondArg),也就是执行你写的函数组件。此时组件内的 useState、useEffect 等 Hooks 开始按顺序执行。
  4. 清理全局状态:组件函数执行完毕后,将 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 时的「流水线」细节

useState 原理:挂载初始化与更新复用 当 Component() 执行、遇到第一个 useState 时,React 内部发生了以下精妙的配合。

🔹 调用第一个 useState(count)

  1. 从 currentHook 拿到旧 Hook1:导航员(currentHook)从旧链表头上摘下了第一颗果实(Hook1,里面记录着旧的 count = 0)。
  2. 处理 Hook1.queue.pending 里的 update:检查 Hook1 身上的「待办事项」(更新队列)。发现有人调用了 setCount(1),所以队列里有个 update。
  3. 算出新 state:count = 1:将队列里的 update 依次执行,计算出最新的状态 1。
  4. 创建新的 workInProgressHook,挂到 workInProgress.memoizedState:记录员(workInProgressHook)在「新画板」上创建一个全新的 Hook 节点(记录为 count = 1)。因为这是第一个 Hook,所以 workInProgress.memoizedState 直接指向它,新链表有了头节点。

🔹 调用第二个 useState(name)

  1. 从 currentHook.next 拿到旧 Hook2:导航员顺着旧链表的 next 指针,拿到了第二个旧 Hook(记录着 name = 'react')。
  2. 复用 state:name = 'react':检查 Hook2 的更新队列,发现没有 setName 的调用,于是直接复用旧状态。
  3. 记录员新建一个新 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 原理

useEffect 原理:render 收集 → commit 调度 → 绘制后执行

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 布局的场景

六、总结与易错点

  1. Hooks 的顺序就是身份:Fiber 上没有 Hook 的名字或 ID,更新时靠「调用顺序 ↔ 链表顺序」一一对应,所以 useState / useEffect 不能写在条件、循环或嵌套函数里。
  2. 两个 memoizedState 不是一回事:Fiber 的 memoizedState 是 Hook 链表的头指针,Hook 的 memoizedState 才是这个 Hook 自己的状态。
  3. setState 是先入队、后计算:调用时只是把 Update 挂进 queue.pending 环形链表并标记优先级,真正算新 state 是在下一轮 render 的 updateState 里。
  4. 连续 setState 用函数式更新更安全:setCount(c => c + 1) 会依次执行队列里的每个 update;写成 setCount(count + 1) 则可能拿到闭包里的旧值。
  5. render 阶段必须保持纯:它可能被打断、丢弃、重来,所以不要在组件函数体里写 DOM、发请求或改全局变量。
  6. render 负责算,commit 负责改,effect 负责补:useEffect 在绘制之后异步执行;需要同步读取 / 修改 DOM 就用 useLayoutEffect。
  7. 双缓存的意义:同时保留 current 和 workInProgress 两棵树,新树算好之前旧树一直挂在页面上,用户看不到半成品。

评论

作品集 →

On this page

  • 目录
  • 一、要掌握的核心知识点
  • 二、整体渲染框架:开始阶段 → render → commit
  • 2.1 四个阶段一览
  • 2.2 ① 开始阶段:setState 只是「登记」
  • 2.3 ② render 阶段:只算,不动手
  • 2.4 ③ commit 阶段:动手改 DOM
  • 2.5 ④ 绘制之后:useEffect 才真正执行
  • 2.6 小结:hook、useState、useEffect、fiber 分别在哪个阶段
  • 三、Fiber 与 Hook 链表
  • 3.1 一个 Counter 组件
  • 3.2 首次渲染后:fiber.memoizedState 指向 Hook 链表
  • 3.3 点击按钮:queue.pending 变成环形链表
  • 3.4 更新渲染时,Fiber 和 Hook 链表怎么配合
  • 3.5 简化内部结构
  • 3.6 为什么 Hook 不能写在条件语句里
  • 四、Hooks 的调用时机:beginWork 阶段
  • 4.1 调用链
  • 4.2 renderWithHooks 内部流程(准备开工)
  • 4.3 调用 useState 时的「流水线」细节
  • 4.4 相关核心函数
  • 五、useEffect 原理
  • 5.1 存储位置:Fiber 的 memoizedState 链表
  • 5.2 render 阶段:创建 effect,对比依赖
  • 5.3 commit 阶段:收集完之后才执行
  • 5.4 清理函数
  • 5.5 和 useLayoutEffect 的区别
  • 六、总结与易错点