先记住这个答案
把循环进度和累计结果保存在任务局部状态中,每次只执行有界数量的步骤;未完成时用 setImmediate 安排下一片,让当前回调返回。这样其他事件有机会在片段之间被处理,总计算仍在同一线程进行,时间复杂度也不会因为切片自动下降。片段大小需要结合每步成本和响应目标选择,不能只按条数保证毫秒期限。还要处理输入上限、取消和异常收尾,避免用 Promise 微任务反复续接却没有真正给 I/O 让出循环机会。
- 保存游标,下一片从未完成位置继续
- 片段间让出机会,片段内仍同步执行
- 总计算量不变,额外增加调度开销
把一口气完成改成多次有界推进
普通大循环会在返回前持续占用当前线程,其他回调只能等待。切片把下一段放到后续事件循环机会,关键是当前片段必须真正结束,而不是在同步函数内部继续递归调用自己。
任务局部游标可防止多次并发调用互相覆盖进度。错误或取消也应围绕这份任务状态处理,不能把全局变量当作所有请求共用的累加器,否则调度交错会引入结果串扰。
用求和示例核对结果与让出机会
下面累加从零到 count 减一的整数,限制输入和每片数量,避免示例出现无界任务。首片在 Promise executor 中同步执行,之后的片段才通过 setImmediate 继续,语法上的 Promise 不会让首片自动异步化。
求和总时间复杂度仍为线性,额外状态为常数,调度次数随片段数量增长。测试应同时验证数学结果和外部 immediate 能在完成前执行,不能只检查最终值就宣称已经改善响应性。
export function sumRange(count: number, chunkSize = 1000): Promise<number> {
if (!Number.isSafeInteger(count) || count < 0 || count > 1_000_000 ||
!Number.isSafeInteger(chunkSize) || chunkSize < 1 || chunkSize > 10_000) {
return Promise.reject(new RangeError('invalid count or chunkSize'));
}
let index = 0;
let total = 0;
return new Promise(resolve => {
function step() {
const stop = Math.min(count, index + chunkSize);
for (; index < stop; index++) total += index;
if (index < count) setImmediate(step);
else resolve(total);
}
step();
});
}当前上限使整数求和仍处于安全整数范围,结果不依赖浮点近似比较。它是调度教学示例,实际业务还需在片段边界加入取消与错误处理,并根据每步成本选择预算。
片段划分不能代替更好的算法和隔离
若某一步本身调用长同步函数,即使每片只有一步,仍会造成长时间占用。应寻找更细的可恢复步骤或使用独立执行资源;切片只在工作能被合理拆分时有效。
同样,不断 await 已完成 Promise 可能主要在微任务队列中续接,不能等同于本例的循环让出。完成后要复测真实请求延迟和总耗时,确认响应改善没有以不可接受的总时长或队列积压为代价。
容易答错的地方
- 把下一片直接递归调用
- 直接调用会继续占用当前调用栈,无法提供预期的调度机会,还可能造成栈深问题。应将后续片段交给合适调度机制,并验证其他任务确实能够在完成前执行。
- 认为切片一定让总计算更快
- 总工作量没有自动减少,调度还会增加开销。主要收益是响应性和公平性,应同时报告总耗时与其他任务延迟;需要加速时还要考虑算法改进或真正并行执行。
面试官还会怎么问?
怎么选择每片多少条?
先测量代表性输入下单步和片段耗时,结合允许的响应延迟选择,并考虑最坏数据分布。固定条数只是容易实现的预算方式,成本差异大时需要时间预算或进一步拆分。
取消应该放在哪里检查?
通常在片段开始或继续安排前检查取消状态,停止后续生产并结算任务。已完成的外部副作用不能靠停止游标自动回滚,若计算涉及写入,还需独立定义补偿与幂等。
什么时候应该放弃切片改用 Worker?
如果单步难拆、总 CPU 工作很大或需要多核并行,工作线程可能更合适。仍需比较传输和排队成本,并控制执行器数量;切片与卸载是按任务性质选择的工具,不是固定升级顺序。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。