生成器天然适合时间分片,因其函数执行到 yield 即暂停并交出控制权,形成天然分片边界;配合事件循环调度,每帧只执行一小段并主动让出主线程,实现流畅的长任务处理。

用生成器实现时间分片,核心是把长任务变成可中断、可恢复的步骤流,再配合事件循环调度,在每帧之间只执行一小段,主动让出主线程。它不是靠“等多久”,而是靠“做多少就停一次”。
为什么生成器天然适合时间分片
生成器函数(function*)返回一个迭代器,调用 next() 就执行到下一个 yield,然后暂停并交出控制权。这个“暂停点”就是天然的分片边界——你可以在每次 yield 后插入异步调度,让浏览器有空处理渲染、输入和动画。
基础实现:手动调度 + setTimeout
这是最易理解、兼容性最好的方式:
- 把计算逻辑写成生成器,每处理一批数据(如 50 个元素)就 yield 一次
- 用递归 + setTimeout(..., 0) 触发下一轮 next(),确保进入宏任务队列尾部
- 避免在 for 循环里连续调用 setTimeout,防止任务堆积
示例:
function* heavyCalc(items) {
for (let i = 0; i function runSliced(generator, onResult, done) {
const iter = generator();
function next() {
const { value, done: isDone } = iter.next();
if (isDone) return done?.();
onResult(value);
setTimeout(next, 0); // 关键:让出主线程
}
next();
}<p>// 使用
runSliced(() => heavyCalc(largeArray), console.log, () => console.log('完成'));
</p>
进阶方案:结合 requestIdleCallback 更精准控时
比起 setTimeout(0),requestIdleCallback 能真正利用浏览器空闲时段,更适合后台计算类任务:
- 在回调中用 while (deadline.timeRemaining() > 1) 循环调用 next(),直到快没时间了才退出
- 必须加总耗时上限(如 500ms),防止碎片化拖太久
- 不支持时降级到 setTimeout(0) 或 queueMicrotask,但注意后者不保证空闲时机
关键不是“调用它”,而是“在它给的时间内尽量多走几步”。
增强控制:加入暂停、取消与优先级
真实场景需要更健壮的调度器:
- 用 class TimeSlicer 管理多个生成器任务队列,按帧预算(如 12ms)分配执行时间
- 每个 next() 前检查 performance.now(),超时立即 yield 并挂起任务
- 暴露 pause() / resume() / cancel() 方法,便于响应用户操作(如滚动时暂停渲染)
- 高优任务可插队,低优任务延后到 idle 阶段
这类封装把“分片逻辑”和“业务逻辑”解耦,后续扩展中断恢复、进度上报都更容易。
注意事项与避坑点
生成器分片不是银弹,需注意:
- yield 的值不能是 DOM 节点或大型对象,避免内存泄漏;建议只传轻量数据或索引
- 不要在生成器内部做同步 DOM 修改(如 appendChild),应 yield 后由调度器统一提交,减少重排重绘次数
- 状态必须保留在生成器闭包或外部变量中,yield 不会自动保存局部作用域上下文
- 若任务含异步依赖(如 fetch),需改用 async generator(async function*)并 await yield
本质上,生成器提供的是“可控暂停”的能力,而时间分片的效果,取决于你怎么调度它、何时让出、以及让出多久。










