React 18 的并发调度我是从MessageChannel和setTimeout的差别看进去的,再补上任务怎么打断、怎么恢复,才理解时间切片在做什么。
Q1: 异步调度的历史?为什么要有它?
1
2013年: 同步渲染的瓶颈
React 诞生之初采用同步渲染——当状态变化时,整棵虚拟 DOM 树一次性协调完成。如果组件树很大(如 10000 个节点),协调过程可能耗时 100ms 以上,期间页面完全阻塞,用户点击无响应。
2
2017年: Fiber 架构的诞生
React 16 引入 Fiber 架构,把协调工作拆分成一个个小单元(Fiber 节点)。每个单元执行完后,检查是否需要让出主线程——如果用户正在输入或有高优先级任务,就暂停当前工作,先处理高优先级任务。
3
2018-2022年: Scheduler 独立成包
React 16.6 起把调度逻辑拆成独立的
scheduler 包,实现时间切片和优先级队列。React 18(2022 年)在此基础上启用并发渲染,把 startTransition、useDeferredValue 等更新接入这套调度器。异步调度 = 优先级队列 + 时间切片,让高优先级任务不被低优先级任务阻塞。
🚫 没有异步调度
一家医院没有分诊台,所有患者按先来后到排队。一个感冒患者排在前面,后面的心脏病患者只能等着——直到感冒患者看完,心脏病患者可能已经错过了最佳抢救时间。
✅ 引入异步调度
有了分诊台(调度器),护士根据病情严重程度分配优先级。心脏病患者直接进抢救室(高优先级),感冒患者继续排队(低优先级)。每个人都能在合理的时间内得到治疗。
Q2: 什么是时间切片(Time Slicing)?
时间切片的核心思想:把一个长任务拆成多个小单元,每个单元执行完后检查是否需要让出主线程。MessageChannel vs setTimeout 的调度优先级
React Scheduler 的时间切片实现
React Scheduler 内部用shouldYieldToHost() 判断是否需要让出主线程:
Q3: 优先级队列的实现
异步调度的核心是优先级队列——不是所有任务都平等,高优先级任务可以插队。
注意:这是 Scheduler 的 5 档优先级(每档对应一个过期时间),和 React 内部的 Lane 模型(用位掩码标记更新批次)是两层概念,不要混为一谈。
小顶堆(Min Heap)实现优先级队列
React Scheduler 内部用小顶堆实现优先级队列,保证每次取到的都是最高优先级任务:优先级队列 vs 普通队列
Q4: requestAnimationFrame 与调度的关系
requestAnimationFrame 是浏览器提供的帧同步机制,它在每一帧渲染前执行回调。
rAF 的取消机制
Q5: Vue 的 nextTick 与调度机制
Vue 的nextTick 本质上也是一种异步调度——把 DOM 更新推迟到微任务队列清空后执行。
Vue 的异步队列 vs React 的优先级队列
Q6: 如何实现一个简易的异步调度器?
面试中经常考”实现一个带并发限制的异步调度器”。带优先级的异步调度器
Q7: AbortController 与任务取消
异步调度中,任务取消是关键能力——当高优先级任务到来时,需要取消低优先级任务。Q8: Web Workers 与真正的并行计算
当任务是 CPU 密集型(如大数组排序、图像处理)时,异步调度无法解决问题——因为 JS 是单线程的。此时需要用 Web Workers 实现真正的并行。🔍 底层源码:React Scheduler 是如何实现的?
React Scheduler 的时间切片是怎么实现的?
React Scheduler 内部用 MessageChannel 实现时间切片(详见 React Scheduler 源码:packages/scheduler/src/forks/Scheduler.js):
- 用
MessageChannel替代setTimeout(更精确的宏任务调度) - 每帧最多执行 5ms(yieldInterval),剩下的时间留给浏览器渲染
- 用
shouldYieldToHost()判断是否需要让出主线程
Vue 的调度器和 React 的调度器有什么本质区别?
Vue 的调度器(queueJob)本质上是一个微任务批处理机制,而 React 的调度器是一个优先级时间切片机制:
- Vue:不能中断,所有任务一次性执行完(微任务必须清空)
- React:可以中断,每个 Fiber 节点执行完后检查是否需要让出主线程
及格回答 vs 满分回答
问题:“说说异步调度,React 的并发模式是怎么实现的?”
及格回答(60-70分):“React 18 引入了并发模式,用 Scheduler 实现时间切片。把长任务拆成小块,每块执行完后让出主线程。还有优先级队列,高优先级任务可以打断低优先级任务。”问题出在:知道概念但不理解实现。 满分回答(90-100分):
异步调度解决的核心问题是高优先级任务被低优先级任务阻塞。在同步渲染模式下,状态变化触发整棵组件树的协调,如果组件树很大,主线程会被阻塞 100ms 以上,期间用户输入无响应。[1]
React 18 的 Scheduler 用 MessageChannel 实现时间切片——每帧最多执行 5ms 的 JS 任务,剩下的时间留给浏览器渲染和响应用户输入。内部用小顶堆实现优先级队列,5 种优先级(Immediate > UserBlocking > Normal > Low > Idle)对应不同的超时时间。高优先级任务到来时,可以打断正在执行的低优先级任务。[2]
实际项目中的应用:用startTransition标记低优先级更新(如搜索结果),用useDeferredValue延迟非紧急的渲染。在搜索框场景中,输入框用同步更新(立即响应),搜索结果用startTransition包裹(可以被打断),用户始终看到最新的输入,搜索结果异步更新。[3]
Vue 的调度机制不同——它用微任务批处理,不能中断。Vue 3 的nextTick本质上是Promise.resolve().then(flushJobs),所有状态变化被收集到队列中,最后一次性更新 DOM。这种机制简单高效,但无法像 React 那样根据优先级中断和恢复任务。
常见追问
React 的时间切片是怎么实现的?
React 的时间切片是怎么实现的?
答题要点:
用 MessageChannel 替代 setTimeout,每帧最多执行 5ms。高分回答:
“React Scheduler 用 为什么用
MessageChannel 实现时间切片。MessageChannel 的回调作为宏任务执行,但比 setTimeout 优先级更高。每帧最多执行 5ms(yieldInterval),超过 5ms 就让出主线程。MessageChannel 而不是 setTimeout?setTimeout在嵌套达到 5 层后会被钳制到最小 4ms,本身也有调度延迟setTimeout的回调执行时机受任务队列影响,不够精确MessageChannel的 postMessage 立即入队,时间片计算更稳定”
React 的优先级是怎么划分的?
React 的优先级是怎么划分的?
答题要点:
Scheduler 的 5 档优先级负责”何时执行”,Lane 位掩码负责”哪些更新可以合并”。高分回答:
“React 里有两层机制,别混为一谈:面试时先讲 Scheduler 的 5 档超时,再说明 Lane 是 React 内部的更新标记模型,就不会把两个概念搅在一起。”
- Scheduler 优先级:Immediate(-1ms)、UserBlocking(250ms)、Normal(5000ms)、Low(10000ms)、Idle(1073741823ms),决定回调什么时候执行、何时过期。
- Lane 模型:React 自己用 31 位掩码给每个更新打标签(
SyncLane、InputContinuousLane、DefaultLane、TransitionLanes、IdleLane等),用于批量合并和跳过更新。Lane 不是”优先级数字”,不直接对应超时时间。
startTransition 和 setTimeout 有什么区别?
startTransition 和 setTimeout 有什么区别?
答题要点:
startTransition 标记低优先级,可以被高优先级打断;setTimeout 延迟到下一帧。高分回答:
“区别:
startTransition 和 setTimeout 的本质区别是:- 时机:
setTimeout在下一个宏任务执行;startTransition在当前帧的低优先级时间片执行 - 中断:
setTimeout的回调一旦执行就不能中断;startTransition标记的更新可以被高优先级任务打断 - 状态:
startTransition有isPending状态,可以显示加载指示器
Vue 的 nextTick 和 React 的 flushSync 有什么区别?
Vue 的 nextTick 和 React 的 flushSync 有什么区别?
答题要点:
nextTick 是微任务批处理,flushSync 是同步刷新。高分回答:
“核心区别:
Vue.nextTick 和 React.flushSync 的区别:- Vue nextTick:把 DOM 更新推迟到微任务,多次变化合并为一次更新(优化)
- React flushSync:强制同步刷新,立即更新 DOM(用于需要立即看到效果的场景)
- Vue 的
nextTick:读取更新后的 DOM(如获取元素尺寸) - React 的
flushSync:在事件处理函数中需要立即看到 DOM 变化(如焦点管理)”
如何实现一个带并发限制的异步调度器?
如何实现一个带并发限制的异步调度器?
答题要点:
用队列 + 计数器实现,每次最多执行 N 个异步任务。高分回答:
“核心思路:维护一个等待队列和一个正在执行的计数器。实际应用:
- 限制 API 请求并发数(避免触发限流)
- 限制图片上传并发数(避免带宽占满)
- 限制文件下载并发数(避免磁盘 I/O 过载)”
requestIdleCallback 有什么局限性?
requestIdleCallback 有什么局限性?
答题要点:
兼容性差、执行时机不可靠、不适合动画。高分回答:
“React Scheduler 选择用
requestIdleCallback 有三个主要局限性:- 兼容性差:Safari 长期未支持,需要 polyfill
- 执行时机不可靠:如果主线程一直忙碌,回调可能永远不执行
- 不适合动画:回调在浏览器空闲时执行,无法保证在渲染前执行
MessageChannel 而不是 requestIdleCallback,正是因为:MessageChannel兼容性好(IE 10+)MessageChannel的回调在当前宏任务结束后立即执行,时机可控- React 自己实现了时间切片(
shouldYieldToHost),不需要依赖浏览器的空闲判断”
🔥 如果在微任务中不断产生新的微任务,调度器会怎样?
🔥 如果在微任务中不断产生新的微任务,调度器会怎样?
答题要点:
微任务必须清空,会导致主线程永远阻塞,调度器无法工作。高分回答:
“会导致主线程彻底阻塞,调度器完全失效。因为微任务的优先级高于宏任务——必须把所有微任务清空,才能进入下一个阶段。如果微任务无限递归产生新微任务,主线程就永远困在微任务队列中。这就是为什么 React Scheduler 用
MessageChannel(宏任务)而不是 Promise(微任务)——宏任务每次只取一个执行,执行完就有机会让出主线程。”🔥 在实际项目中,如何选择 React 的调度策略?
🔥 在实际项目中,如何选择 React 的调度策略?
答题要点:
根据任务的紧急程度选择不同的调度 API。高分回答:
“React 18 提供了多种调度策略,根据任务的紧急程度选择:选择策略:
- 用户输入:同步更新(立即响应)
- 搜索结果:
startTransition(可以被打断) - 大列表过滤:
useDeferredValue(延迟渲染) - 焦点管理:
flushSync(强制同步)”
🔥 如何实现并发任务控制?async pool 和 p-limit 的原理是什么?
🔥 如何实现并发任务控制?async pool 和 p-limit 的原理是什么?
答题要点:
区分”并发限制”和”并行限制”,手写 async pool 核心逻辑,理解 p-limit 的信号量模型。高分回答:
“并发任务控制的核心是限制同时处于 pending 状态的 Promise 数量,而不是限制同时执行的线程数(JS 是单线程的)。常见实现有三种思路:思路 1:Promise.race 动态池(p-limit 原理)维护一个’正在执行’的 Set,池满时用 思路 2:信号量(Semaphore)模型(p-limit 库的实现)用一个计数器模拟信号量,每获取一个令牌执行一个任务,任务完成后归还令牌:思路 3:Generator 顺序喂入用 Generator 逐个产出任务,调度器从 Generator 中取任务喂入执行池:p-limit vs asyncPool 的核心区别:
Promise.race 等待任意一个完成后再加入新任务:p-limit是通用限流器,返回一个limit函数,可以在任何场景复用,支持排队(queue)asyncPool是一次性批处理,传入数组和并发数,返回全部结果,不保留队列状态
- 批量上传文件(限制同时上传 3 个)
- 批量请求接口(避免触发服务端限流)
- 批量 Puppeteer 截图(限制同时打开的页面数)“
延伸阅读与引用参考
- 🔗 React 18 并发模式官方文档:React 团队对并发特性的权威解释,涵盖
startTransition、useDeferredValue等核心 API。[1] - 📖 React Scheduler 源码解析:Scheduler 的完整实现,包括时间切片和优先级队列。[2]
- 🟢 Vue 3 调度器设计:Vue 官方对响应式系统和调度机制的深度解释。[3]
- 📘 Inside React’s scheduling system:Mark Erikson 对 React 渲染和调度行为的全面分析。[4]
