React中,函数组件里执行setState后到UI上看到最新内容的呈现,react内部会经历哪些过程?

一. 背景

作为一名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 保存:

    1. 组件类型(函数组件、类组件、原生 DOM)
    2. 当前状态(state / props / hook)
    3. 子节点 / 兄弟节点 / 父节点指针(构建 Fiber 树)
    4. 更新队列 / 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);
  1. Hook.queue.pending 入队
  2. 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 />);

发生了:

  1. React 创建一个 HostRoot Fiber 对应根容器 DOM:

    kotlin 复制代码
    const rootFiber = {
      tag: HostRoot,
      stateNode: container, // 根 DOM
      child: null,
      return: null,
    };
  2. 这个 Fiber 是整个 Fiber 树的根节点(root.current = null,第一次渲染前没有 Fiber 树)。


2. 调度器触发更新

  • scheduleUpdateOnFiber(rootFiber) 被调用,React 知道要渲染 rootFiber 下的 UI
  • 根 Fiber 的 updateQueue 存储了要渲染的 element(这里就是 <Home />

3. Render 阶段:创建 Home Fiber

  1. 开始 render 阶段

    ini 复制代码
    workInProgress = createWorkInProgress(rootFiber, null)
  2. beginWork(workInProgress) 执行时:

    • current = rootFiber.current → null(第一次)
    • workInProgress.pendingProps = <Home />
  3. 调用 reconcileChildrenworkInProgress.pendingProps 生成子 Fiber:

    • pendingProps<Home /> JSX

    • React 看到 <Home /> 是函数组件 → 创建 Fiber:

      kotlin 复制代码
      const homeFiber = {
        tag: FunctionComponent,
        type: Home, // 对应函数组件
        stateNode: null, // 函数组件没有实例
        child: null,
        return: workInProgress, // parent = HostRoot Fiber
      };
  4. 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得出结论,一点点的提出疑问来完善这个流程。

相关推荐
AGG_Chan7 小时前
flutter专栏--深入剖析你的第一个flutter应用
前端·flutter
再学一点就睡7 小时前
多端单点登录(SSO)实战:从架构设计到代码实现
前端·架构
繁依Fanyi7 小时前
思维脑图转时间轴线
前端
发愤图强的羔羊7 小时前
Chartdb 解析数据库 DDL:从 SQL 脚本到可视化数据模型的实现之道
前端
摸着石头过河的石头7 小时前
控制反转 (IoC) 是什么?用代码例子轻松理解
前端·javascript·设计模式
携欢8 小时前
PortSwigger靶场之Stored XSS into HTML context with nothing encoded通关秘籍
前端·xss
小桥风满袖8 小时前
极简三分钟ES6 - const声明
前端·javascript
小小前端记录日常8 小时前
vue3 excelExport 导出封装
前端
南北是北北8 小时前
Flow 的 emit 与 tryEmit :它们出现在哪些类型、背压/缓存语义、何时用谁、常见坑
前端·面试