这是一个非常深刻的问题,直击了浏览器运行机制的核心。
作为一个 Senior Dev,我们要回答“为什么需要时间切片”,不能只说“为了不卡顿”,必须从浏览器的底层运行机制和JavaScript 的执行模型这两个维度来解释。
简单来说,Scheduler 实现时间切片是为了解决JavaScript 执行与浏览器渲染互斥的问题。
以下是详细的深层逻辑:
1. 根本矛盾:GUI 渲染线程与 JS 引擎线程是互斥的
在浏览器中,JavaScript 的执行和页面的绘制(Layout, Paint) 是运行在同一个主线程(Main Thread)上的。这就像是一个只有一个窗口的办事大厅:
- 窗口里只有一个人(主线程)。
- 他既要负责处理逻辑(执行 JS 代码,React 的 Diff 算法)。
- 又要负责画图(浏览器重排、重绘、响应动画)。
互斥意味着: 当 JS 在执行时,浏览器就无法绘制,也无法响应用户的点击;反之亦然。
2. “16.6ms 黄金法则”与长任务(Long Task)
为了让用户感觉页面是“流畅”的,屏幕刷新率通常是 60Hz,也就是说,每一帧的预算只有 16.6ms(1000ms / 60 ≈ 16.6ms)。
在这 16.6ms 里,浏览器需要做很多事:
- 执行 JS 脚本。
- 样式计算(Style Calculation)。
- 布局(Layout)。
- 绘制(Paint)。
- 合成(Composite)。
如果没有时间切片(React 15 及以前): 假设你的 React 应用组件树很深,一次更新(Diff + Patch)需要计算 50ms。 因为 JS 执行是同步不可中断的(Run-to-completion),在这 50ms 内,主线程一直被 React 占用。
- 结果: 浏览器错过了一次甚至两次画面刷新(掉帧)。
- 用户感知: 页面卡顿、点击按钮没反应、输入框文字出不来。这就是所谓的“长任务阻塞”。
3. 时间切片(Time Slicing)的解法
Scheduler 的核心目标就是:打破“执行完才能停”的规则。
它把那个 50ms 的大任务,切成 10 个 5ms 的小任务:
- 执行 5ms (React 工作): 比如 diff 了 50 个组件。
- Scheduler 检查时间: “哎呀,5ms 到了,该把主线程交还给浏览器了(Yield to host)。”
- 浏览器喘息: 浏览器利用这个间隙去处理点击事件、去绘制一帧动画。
- 继续执行: 浏览器忙完了,Scheduler 再把 React 叫回来,继续 diff 剩下的组件。
这样做的结果是: 虽然处理完整个任务的总时间可能从 50ms 变成了 55ms(因为有切换开销),但在用户看来,界面始终是活的,每 16ms 都能刷新一次,完全感知不到卡顿。
4. 为什么 Scheduler 要自己实现?(技术深挖)
你可能会问:“JS 不是有 setTimeout 吗?浏览器不是有 requestIdleCallback 吗?为什么要 React 自己写一个 Scheduler?”
这也是面试中常考的高级点:
(1) requestIdleCallback 的缺陷
浏览器原生提供了 requestIdleCallback (rIC),它的初衷就是在“浏览器空闲”时执行低优先级任务。React 最初确实想用它,但遇到了两个问题:
- 兼容性: iOS Safari 很多版本不支持,且不稳定。
- 触发频率不稳定: rIC 只有在浏览器真的“没事干”时才触发。如果用户一直在滚动页面,或者有高频动画,rIC 可能一秒钟只能执行很少几次(甚至只有 20fps)。这会导致 React 的更新迟迟不执行。
(2) setTimeout 的精度问题
如果用 setTimeout(fn, 0),浏览器会有 4ms 的最小延迟(在嵌套层级深时)。对于追求极致性能的 React 来说,每切一片浪费 4ms 是不可接受的。
(3) React Scheduler 的实现 (MessageChannel)
React 团队最终选择了使用 MessageChannel 配合 requestAnimationFrame (早期) 或者微任务机制来模拟。
- 它创建了一个宏任务(MacroTask)。
- React 默认给每个任务切片分配 5ms 的时间。
- 它使用
performance.now()高精度计算当前任务执行了多久,一旦超过 5ms,就通过MessageChannel发送消息,触发下一个宏任务,从而把控制权**让出(Yield)**给浏览器,让浏览器有机会在这个间隙插入绘制任务。
总结
为什么 Scheduler 需要实现时间切片?
- 物理限制: JS 线程和 UI 渲染线程互斥。
- 体验底线: 必须在 16.6ms 内归还控制权给浏览器,否则就会掉帧(卡顿)。
- 架构需求: React 的更新往往是计算密集型的(CPU Bound),必须通过切片把“大山”炸碎成“石子”,才能在单线程的 JS 世界里实现“并发”的错觉,保证高优先级的交互(如输入)永远能插队执行。