【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 响应更快)来换取 页面流畅度和用户体验

相关推荐
SoaringHeart2 小时前
Flutter进阶:自定义一个 json 转 model 工具
前端·flutter·dart
努力打怪升级2 小时前
Rocky Linux 8 远程管理配置指南(宿主机 VNC + KVM 虚拟机 VNC)
前端·chrome
brzhang3 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang3 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
reembarkation3 小时前
自定义分页控件,只显示当前页码的前后N页
开发语言·前端·javascript
gerrgwg3 小时前
React Hooks入门
前端·javascript·react.js
ObjectX前端实验室3 小时前
【react18原理探究实践】调度机制之注册任务
前端·react.js
汉字萌萌哒4 小时前
【 HTML基础知识】
前端·javascript·windows
ObjectX前端实验室4 小时前
【React 原理探究实践】root.render 干了啥?——深入 render 函数
前端·react.js