一. 背景
作为一名react开发,每天都在和setState打交道,当我们要更新某个值并且UI上需要和这个值关联,那么需要用到setState,这是表象,那我也很好奇执行setState后到底发生了什么才会导致UI的变化,脑中多多少少会浮现一些概念,例如虚拟dom,diff算法,批量更新,还有react 16后的Fiber,延伸到时间分片,可中断,可恢复等,如果能想到这些,说明还是有尝试去了解过react框架的原理。我们不妨再提出一些疑问,setState只是更新一个值而已,是怎么和UI扯上关系了?setState执行后表象看只是修改了值,那怎么触发了diff的对比? 接下来我们就详细聊聊。
二. 具体过程
每一次的渲染都会经历两个大的过程,第一个是render阶段,这个阶段的结果就是生成diff的差异树;第二个阶段commit阶段,基于上一个阶段的差异树去修改真正的dom,渲染到页面上,并且执行一系列useEffect和ref的更新。render阶段里涵盖了大部分的核心概念,它是异步的,而第二个阶段commit,是同步的,必须一次性更新完。所有例如useEffect这些钩子函数都是在render后才会执行,而state值也是在render后才能拿到,当然你也可以马上拿到,用flusync包裹住就可以,它能强制同步执行更新。
了解完大的概念后,现在细化步骤。
1) 入队:setState
发生了什么 ?
这一步入队,如果不去查资料,大部人会忽略这个过程,但是它也是整个流程的第一步,没有它无法串联整体。 当我们执行setState
实质是把这个更新操作放到react内部的更新队列中去,PS(队列,先进先出,类比排队买票,第一个排队的先买到,保证按顺序执行) 类组件的setState和函数组件的useState会有不同的入队逻辑,这里我们只讨论函数组件的做法。
在说这个之前先说说fiber的概念,才能往下讲,Fiber是react16及之后版本出的概念,一直在逐步完善,到现在React19已经完全成熟,它用链表来替代以前的递归树的方式。
Fiber 的概念
Fiber = React 内部用来表示组件的单元工作(Unit of Work)
-
每个组件、每个 DOM 节点对应一个 Fiber 对象
-
Fiber 保存:
- 组件类型(函数组件、类组件、原生 DOM)
- 当前状态(state / props / hook)
- 子节点 / 兄弟节点 / 父节点指针(构建 Fiber 树)
- 更新队列 / effect list(commit 阶段更新 DOM 的计划)
-
Fiber 的设计核心:可中断、可分片、优先级调度
简化结构:
go
Fiber = {
type, // 组件类型
stateNode, // 组件实例或 DOM 节点
child, sibling, return,// 构建树
memoizedState, // Hook 或类组件 state
updateQueue, // 更新队列
effectTag, // 标记 DOM 更新类型
nextEffect, // Effect 链表
}
每个函数组件都是个Fiber节点,而函数组件里每个hook对应这Fiber上的hook链表节点,那hook链表又是存放在哪?
yaml
Hook = {
memoizedState: 当前 state,
queue: UpdateQueue,
next: Hook | null
}
UpdateQueue = {
pending: Update | null
}
Update = {
action: 新的 state 或 updater 函数,
next: Update | null
lane: 优先级 // 影响调度时机
}
Fiber.memoizedState
→ 指向第一个 Hook- 每个 Hook 内部有自己的
queue.pending
链表(循环链表实现)
此时setState就存放在自己hook链表节点的queue队列里。 这里又产生一个新的疑问,是入队了,那是怎么往下执行的?是的,当更新操作入队后还会给对应的Fiber打上"脏"的标记,其实就是更新update对象里lane(React 18完全成熟引入和开放)的值,同时通知Scheduler调度器安排合适的时机执行render阶段。
函数组件
scss
setCount(2);
- Hook.queue.pending 入队
- Fiber 上标脏:
scss
markUpdateLaneFromFiberToRoot(fiber, lane)
- 从当前 Fiber 向上找根节点(RootFiber)
- 在根节点的
pendingLanes
记录本次更新的 lane - 根节点就是 Scheduler 调度入口
2) 调度Scheduler:通知去执行render流程,但是决定何时以什么节奏去执行
先看看Scheduler的发展史,它是任务调度的核心。
1️⃣ React 15 及以前(Stack Reconciler)
-
递归渲染整棵树
-
同步更新 :一旦
setState
被调用,React 就会从根节点递归 render 整棵树 -
问题:
- 大组件渲染阻塞主线程 → 卡 UI
- 无法中断渲染 → 用户输入延迟
- 优先级调度不可行
-
调度机制:几乎没有,更新按调用顺序立即执行
2️⃣ React 16(Fiber 架构引入)
-
核心目标:可中断渲染(interruptible rendering)、分片渲染(time-slicing)
-
Fiber 的出现:
- 每个组件 / DOM 节点对应一个 Fiber 对象
- Fiber 保存 state、更新队列、effect list
- 渲染变成 每个 Fiber 一个工作单元 → 可暂停/恢复
-
调度机制:
- Scheduler 核心概念开始出现
- 初期主要支持 时间分片 + 可中断 render
- 优先级调度尚未完全完善
3️⃣ React 16.3--16.8
- 引入 Hook(16.8)
- 函数组件的 state 更新依赖 Fiber.memoizedState + queue
- Scheduler 可以根据更新的 lane / 优先级调度 render
- 开始支持 批处理事件源内更新
4️⃣ React 17
- 自动批处理只在 React 自己的合成事件里生效
- Scheduler 仍然以 Fiber 为单位调度
- 低优先级更新(宏任务 / setTimeout)仍然独立 render
5️⃣ React 18(调度机制成熟)
-
Automatic Batching(自动批处理) :
- 不仅在合成事件内,跨微任务也能批处理
- 多个 setState → 一个 render
-
并发模式(Concurrent Mode / Concurrent Features) :
- 基于Fiber
- Scheduler 支持 优先级 lane
- 可中断 render → 时间切片(time-slicing)
- 高优先级任务(用户输入)可中断低优先级任务
-
调度工具:
- 微任务
queueMicrotask
/Promise.then
→ 高优先级立即 flush - requestIdleCallback / polyfill → 空闲时间渲染低优先级任务
- 微任务
React 18 调度 & 渲染总流程
scss
用户多次调用 setState
┌──────────────────────────────┐
│ 同一事件循环内 → 自动批处理 │
│ 多个 update 合并为一次调度任务 │
└───────────┬──────────────────┘
▼
createUpdate()
enqueueUpdate()
│
▼
scheduleUpdateOnFiber(fiber, lane)
│
▼
ensureRootIsScheduled(root)
│
▼
scheduleCallback(priority, performConcurrentWorkOnRoot)
│
▼
┌───────────────────────────┐
│ Scheduler 阶段 (任务调度器) │
│ - 排序任务队列 │
│ - 选择最高优先级任务执行 │
│ - 新高优先级任务会取消低优先级 │
│ → 重新发起调度 (插队) │
└───────────┬───────────────┘
▼
执行 performConcurrentWorkOnRoot(root)
│
▼
renderRootConcurrent(root)
│
▼
workLoopConcurrent()
┌──────────────────────────────┐
│ 渲染过程 (可中断/恢复) │
│ - 每处理一个 Fiber 调用 shouldYield()│
│ - 如果需要让出 (帧到期/高优先级进来) │
│ → 停止渲染,中断退出 │
│ - 下次调度时从断点继续 │
└──────────────────────────────┘
│
▼
render 阶段完成 → 生成新 Fiber 树
│
▼
commitRoot(root)
(更新 DOM)
调度器 (Scheduler) 做的事
- 接收任务(React 提交的 render 任务)
- 按优先级排序
- 在浏览器空闲时 / 下一帧回调时执行任务函数
它不是主动去触发render阶段,而是合适的调度时机下执行render的回调,也就是这一句: scheduleCallback(priority, performConcurrentWorkOnRoot),开始进入render阶段
⚠️ 它不会自己打断 render,它只是会在"下一个调度点"决定要不要继续执行任务。
4) 渲染阶段(Render):构建Fiber树(可被打断/恢复重执行),diff生成差异树(Effect List)
终于进入render阶段,这一阶段的结果是要获取到最新的虚拟dom。这里第一个疑问,第一个阶段入队,当时把更新操作存放到队列,此时这些state的更新后的值什么时候使用?拿到这些新值后怎么和UI结合?
这个阶段会基于上一次的提交后的state的值去遍历执行updateQueue,只有queue的lane和本次render的lane一致的才执行,其他跳过,此时就能获取到最新的state,然后执行整个函数组件拿到最新的JSX生成的React Element树,使用这颗React Element树和current fiber树进行通过深度遍历的diff对比生成对应的Fiber树,而这颗新树就是workInProgress fiber树,对比过程中会记录要更新的fiber节点并形成一颗Effect List树,这就是render阶段要产出的实际结果。
React Fiber 渲染阶段流程图(Render Phase)
scss
┌──────────────────────────────┐
│ 更新触发 │
│ setState / props / 高优先级任务 │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ 调度器 scheduleUpdateOnFiber │
│ - 将更新加入 fiber.updateQueue │
│ - 按优先级调度任务 │
│ - 合并同一事件循环的多个 setState │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ 创建 workInProgress Fiber 树 │
│ - 克隆 current Fiber → workInProgress │
│ - 准备渲染阶段构建子 Fiber │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ DFS 遍历 workInProgress Fiber 树 │
│ performUnitOfWork(Fiber) │
│ ├─ beginWork(Fiber) │
│ │ ├─ 获取最新 pendingProps │
│ │ ├─ 遍历 updateQueue → 计算 memoizedState(最新 state) │
│ │ ├─ 执行函数组件 / render() → 返回 ReactElement │
│ │ └─ 构建子 Fiber 树 (JSX → Fiber) │
│ ├─ completeWork(Fiber) │
│ │ ├─ 对比 current Fiber → 生成 flags │
│ │ │ ├─ Placement → 新增 DOM 标记 │
│ │ │ ├─ Update → 更新 DOM 属性/文本 │
│ │ │ └─ Deletion → 删除 DOM │
│ │ ├─ 收集子树 effect list → 合并到父节点 │
│ │ └─ 如果自身有 flags → 插入 effect list 尾部 │
│ └─ 检查 shouldYield() → 超过时间片可暂停渲染 │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ DFS 遍历完成 workInProgress 树 │
│ - workInProgress Fiber 树已构建完成 │
│ - effect list 已收集完成 │
│ - commit 阶段准备执行 │
└──────────────────────────────┘
以上流程注意workInProgress Fiber 树最开始的是由Current树拷贝而来的,通过遍历fiber执行fiber上的updateQueque去更新fiber节点,而workInProgress Fiber 的 alternate
→ current Fiber,这样就能开始diff对比。
此外Fiber树是UI的快照,只构建当前在dom里的内容,未加载的路由是不会纳入构建范围的。
这里还涉及两个关键点:
1、diff算法
React 使用 "同级比较" + "最小更新" 的策略(O(n) 性能优化):
同级节点才会比较(父子不同级不会对比)
判断节点是否可复用:
markdown
- **type 不同 → 不能复用**(必须销毁旧 Fiber,创建新 Fiber)
- **key 不同 → 不能复用**(即使 type 相同,也视为不同节点,需要移动或替换)
换句话说:
同级节点必须同时 type 和 key 匹配才能复用 Fiber
所以key的稳定性对于优化至关重要,以及当我们共用一个组件,当不同props对这个组件有不同的影响,可以通过更新key来直接销毁再新建,比在组件内部去判断各种状态要更方便和安全。
2.检查 shouldYield()
shouldYield
是 并发模式(Concurrent Mode)下的时间片调度机制 核心函数,用于判断 是否该暂停当前渲染 ,把控制权交还给浏览器,以保持界面响应流畅。每处理完一个 Fiber 节点或一小段任务时,会调用 shouldYield()
ini
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
如何判断时间片用完
shouldYield
内部通常依赖 浏览器提供的时间 API:
- React 18 使用
Scheduler
库,基于performance.now()
或MessageChannel
- 每个时间片默认约 5ms(可配置)
- 判断逻辑:
arduino
function shouldYield() {
const currentTime = Scheduler.now();
return currentTime >= deadline; // deadline = 时间片结束时间
}
当浏览器有更高优先级任务(如用户输入事件)时,也会提前返回 true
6) 提交阶段(Commit):一次性把变更更新到真实的dom上(不可中断)
基于上面生成的Effect List,终于进到最后的阶段,也是把变化的虚拟dom更新到真实dom上,最后呈现最新的内容在页面上。
总流程概览
scss
render 阶段完成(effect list 已构建)
│
▼
commitRoot(rootFiber)
│
├─ commitBeforeMutationEffects(root) // 执行 getSnapshotBeforeUpdate
│
├─ commitMutationEffects(root) // 更新 DOM / 插入 / 删除 / 更新 props
│
├─ commitLayoutEffects(root) // 调用生命周期和 useLayoutEffect
│
└─ 完成后切换 workInProgress → current,effect list 清空,flags 重置
可以见到,只有完成所有阶段才调用钩子函数拿到最新的state。
以上都是基于某次setState来推论的,那当我们首次打开页面,这时候current fiber树是没有的,那WIP的初始化应该也没有,这时候是怎么构建首次fiber树?
第一次肯定是要新建fiber.
1. 根 Fiber(HostRoot)是入口
当你调用:
scss
ReactDOM.createRoot(container).render(<Home />);
发生了:
-
React 创建一个 HostRoot Fiber 对应根容器 DOM:
kotlinconst rootFiber = { tag: HostRoot, stateNode: container, // 根 DOM child: null, return: null, };
-
这个 Fiber 是整个 Fiber 树的根节点(root.current = null,第一次渲染前没有 Fiber 树)。
2. 调度器触发更新
scheduleUpdateOnFiber(rootFiber)
被调用,React 知道要渲染rootFiber
下的 UI- 根 Fiber 的
updateQueue
存储了要渲染的 element(这里就是<Home />
)
3. Render 阶段:创建 Home Fiber
-
开始 render 阶段:
iniworkInProgress = createWorkInProgress(rootFiber, null)
-
beginWork(workInProgress)
执行时:current = rootFiber.current
→ null(第一次)workInProgress.pendingProps = <Home />
-
调用
reconcileChildren
对workInProgress.pendingProps
生成子 Fiber:-
pendingProps
是<Home />
JSX -
React 看到
<Home />
是函数组件 → 创建 Fiber:kotlinconst homeFiber = { tag: FunctionComponent, type: Home, // 对应函数组件 stateNode: null, // 函数组件没有实例 child: null, return: workInProgress, // parent = HostRoot Fiber };
-
-
Home Fiber 就这样诞生了,是 HostRoot 的 child
核心:Fiber 是在 reconcileChildren 阶段根据 JSX 类型创建的。
4. 构建子 Fiber 树
-
执行
Home()
→ 返回 JSX:xml<Box> <Header /> <List /> </Box>
-
reconcileChildren(Home Fiber, JSX)
→ 为 Box、Header、List 创建对应 Fiber -
Fiber 树的 child/sibling/return 指针全部建立
-
这就是第一次 workInProgress Fiber 树
5. 总结 Home Fiber 的来源
步骤 | 说明 |
---|---|
入口 | ReactDOM.render() → 创建 HostRoot Fiber |
调度 | scheduleUpdateOnFiber(rootFiber) → 标记根更新 |
render | beginWork + reconcileChildren → 根据 <Home /> JSX 创建 FunctionComponent Fiber |
结果 | Home Fiber 成为 HostRoot Fiber 的 child,保存 type = Home,stateNode = null |
核心点:Fiber 不是预先存在的,而是 React 在渲染阶段根据 JSX 动态创建。第一次没有 current Fiber,也不影响它的生成。
以上就是整个阶段的解析,大部分都是基于询问AI得出结论,一点点的提出疑问来完善这个流程。