【react18原理探究实践】异步可中断 & 时间分片

本篇我们看一下,React 从 v15 到 v16 的更新机制演进 ,重点是 递归同步更新 → 时间分片 + 可中断更新 的变化。


背景

1. React v15:递归同步更新

核心特点

  • React v15 的更新是 同步递归更新

    • 一旦触发状态更新(setState 或 props 改变),React 会从 根节点开始递归遍历虚拟 DOM
    • 对比新旧虚拟 DOM,找到差异,然后更新真实 DOM。
  • 问题

    • 对于大型组件树,递归更新会阻塞主线程。
    • 页面无法响应用户交互或动画,浏览器卡顿明显。
  • 缺点

    • 无法中断渲染。
    • 所有任务都是同步完成,要么成功更新,要么浏览器长时间无响应。

示意

复制代码
更新触发 → 递归 diff → 同步更新 DOM
(整个过程阻塞主线程)

2. React v16:Fiber 架构引入

核心目标

  • 解决 v15 的 阻塞渲染问题
  • 支持 可中断异步渲染,让 React 更加流畅、响应用户操作。

Fiber 的引入

  • Fiber 是 React v16 的内部数据结构:

    • 将组件更新拆分为 一个个小任务单元(fiber)。
    • 每个 fiber 对应组件的一个节点。
  • Fiber 可以 暂停、恢复、优先级调度

  • 由此引入 时间分片(time slicing)异步可中断渲染


3. 时间分片与异步可中断渲染

时间分片(Time Slicing)

  • 浏览器每帧约 16ms(60fps),React 将更新任务拆分成小块。
  • 每次只执行部分 fiber,执行过程中会检查 shouldYield()
  • 如果浏览器忙(主线程占用或高优先级任务),React 会 中断当前任务,等待下一帧继续。
  • 这样用户交互、动画、滚动不会被阻塞。

异步可中断

  • v16 不再一次性递归整个树

  • 每个 fiber 是 可中断的单位

  • Fiber 更新被分配 优先级

    • 高优先级任务(如用户输入)可立即打断低优先级更新。
    • 低优先级任务(如数据渲染)可延后,浏览器空闲时再执行。
  • 结合 Scheduler,React 实现:

    • 同步更新 → 高优先级立即执行。
    • 异步更新 → 根据空闲时间片分批执行,避免阻塞。

示意

rust 复制代码
更新触发 → 创建 fiber 树 → 分片执行 fiber → 可中断 / 恢复
|<--时间分片-->|<--时间分片-->| ...

4. v15 → v16 的本质变化对比

特性 React v15 React v16 (Fiber)
更新方式 同步递归更新 异步可中断更新(时间分片)
线程占用 阻塞主线程 可让出主线程,浏览器渲染不卡顿
用户交互 大型更新阻塞用户操作 高优先级任务可打断低优先级任务
更新单位 整棵树 fiber 单元(组件节点粒度)
优先级管理 高/中/低/空闲优先级分级
效果 更新大时卡顿明显 大型更新可平滑分帧渲染,流畅性高

react的异步调度是如何实现的

React 的异步调度核心目标:解决浏览器卡顿问题,同时保证 UI 更新的流畅性

1. 问题背景

从上面我们知道

  • 为了保证用户体验,React 的更新过程是从 root 开始进行"diff",早期是同步的,会阻塞浏览器渲染。
  • 浏览器一帧大约 16ms(60fps),如果 React 更新占满主线程,页面就会卡顿。
  • 解决思路:将 React 更新任务交给浏览器空闲时间执行,让浏览器优先完成绘制任务,再执行不紧急的更新。

2. 时间分片(Time Slicing)

  • 浏览器每帧执行顺序:
    事件处理 → JS 执行 → requestAnimationFrame → Layout → Paint
  • 空闲时间:如果事件循环结束且没有其他任务,浏览器进入休息状态,此时可以执行不紧急任务。
  • React 利用这种空闲时间,分片执行更新任务。

3. 浏览器空闲时间 API

  • requestIdleCallback:浏览器提供的 API,在空闲时执行回调。

    scss 复制代码
    requestIdleCallback(callback, { timeout })
    • callback:空闲时执行
    • timeout:最大等待时间,防止长时间未执行
  • React 对任务设置五个优先级:

    优先级 超时时间 用途说明
    Immediate -1 立即执行
    UserBlocking 250ms 用户交互相关
    Normal 5000ms 普通更新,如网络请求
    Low 10000ms 可延迟处理的任务
    Idle 非必要任务,可能不执行

4. requestIdleCallback 兼容实现

由于 requestIdleCallback 仅 Chrome 支持,React 自己实现了兼容方案,需要满足两个条件:

  1. 能主动让出主线程,让浏览器去渲染视图。
  2. 每次事件循环只执行一次,任务递归请求下一次时间片。

5.为什么宏任务满足上面两个条件?

先回顾下浏览器事件循环

浏览器事件循环中有两类任务:

  • 宏任务(MacroTask) :setTimeout、setInterval、setImmediate(Node)、MessageChannel、postMessage、I/O、UI 渲染。
  • 微任务(MicroTask) :Promise.then、MutationObserver、queueMicrotask。

事件循环大致流程:

  1. 取一个宏任务执行。
  2. 清空所有微任务。
  3. 更新渲染(浏览器有机会去绘制 UI)。
  4. 回到步骤 1。

需求分析

我们要实现一个类似 requestIdleCallback 的 polyfill,要求:

  1. 主动让出主线程,让浏览器渲染视图
    → 也就是说,不能一直在本轮事件循环中耗时执行。
    → 要把任务放到下一次事件循环,这样浏览器中间就能去渲染。
  2. 一次事件循环只执行一次任务,执行后再请求下一次
    → 不希望和浏览器的渲染"抢时间"。

为什么只有"宏任务"能满足?

  1. 微任务不行

    微任务会在当前宏任务结束后立刻执行 ,而且在浏览器渲染之前就被清空。

    → 如果用 Promise.then 来实现,会一直占用在同一轮循环里,浏览器根本没机会插入渲染。

    → 这就不符合"让出主线程"的要求。

  2. 宏任务可以

    • 宏任务之间,浏览器有机会去更新渲染。
    • 比如用 setTimeout(fn, 0) 或者 MessageChannel 来调度。
    • 每次执行完宏任务后,事件循环会进入渲染阶段,满足了"浏览器有机会渲染"。
    • 而且一次循环只执行一个宏任务,天然保证了"只执行一次"的条件。

举个例子

js 复制代码
function myRequestIdleCallback(callback) {
  return setTimeout(() => {
    const start = Date.now();
    // 给 callback 提供 deadline 参数,模拟 requestIdleCallback 的行为
    callback({
      didTimeout: false,
      timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) // 模拟剩余时间
    });
  }, 0);
}

这样做的效果:

  • setTimeout(宏任务)调度 → 下一次事件循环执行,浏览器中间可以渲染。
  • 每次执行完,再调度下一次 → 保证"一次循环只执行一次"。

✅ 所以结论是:

  • 微任务太快,会阻塞渲染 → 不行
  • 宏任务会让浏览器有机会渲染,而且一次循环只执行一次 → 符合条件

6. 宏任务方案的选择,为什么没选setTimeout

(1) setTimeout(fn, 0)

  • 可以创建宏任务,不阻塞浏览器。
  • 问题:递归执行时,间隔会变为约 4ms,而不是 1ms,效率低,浪费时间。我总共16ms的一次循环时间,你一下消费4ms,那肯定不行啊,react希望精细到1ms

(2) MessageChannel(React 的实现方案)

  • 创建一个消息通道(MessageChannel),管道通信,一边发送另一边接收,通过 port.postMessage 异步通知执行任务。

  • 流程:

    1. React 调用 requestHostCallback 注册更新任务到 scheduledHostCallback
    2. port2.postMessage(null) 发送消息通知 port1
    3. port1.onmessage 被触发,执行 scheduledHostCallback
    4. 执行完后清空 scheduledHostCallback
  • 优势

    • 延迟低,几乎每帧可立即执行。
    • 不阻塞主线程,保证浏览器渲染流畅。
    • 高效实现时间分片调度。

7. 总结

React 异步调度(Scheduler)核心思想:

  • 时间分片:把更新拆分成小任务,一帧一帧执行。
  • 空闲时间利用:浏览器有空闲才执行,保证用户体验。
  • 优先级管理:不同任务根据重要性设定超时时间。
  • 兼容实现:MessageChannel 替代 setTimeout,实现跨浏览器高效异步执行。

React 的调度选择牺牲了部分同步响应(不如 Vue 响应更快)来换取 页面流畅度和用户体验

相关推荐
前端慢慢其修远18 分钟前
fabric.js 中originX originY center设置问题
前端·fabric
im_AMBER32 分钟前
React 04
前端·react.js·前端框架·1024程序员节
fhsWar1 小时前
Vue3 props: `required: true` 与 vant 的`makeRequiredProp`
前端·javascript·vue.js
泷羽Sec-静安2 小时前
Less-1 GET-Error based-Single quotes-String GET-基于错误-单引号-字符串
前端·css·网络·sql·安全·web安全·less
小时前端2 小时前
虚拟DOM已死?90%内存节省的Vapor模式正在颠覆前端
前端·html
Keepreal4962 小时前
Web Components简介及如何使用
前端·javascript·html
jump6802 小时前
TS中 unknown 和 any 的区别
前端
无羡仙2 小时前
AI终于‘看见’网页了!Stagewise让UI修改从‘盲调’变‘指哪打哪
前端
柯腾啊3 小时前
“Script error.”的产生原因和解决办法
前端·javascript·浏览器
沙漠之皇3 小时前
ts 定义重复对象字段
前端