两种饥饿方式
第一种是长时间同步任务:循环、大排序或同步解析。在它返回之前,定时器、I/O 回调和渲染都无法运行。
第二种更隐蔽:微任务链。微任务会在下一个宏任务之前被完全清空。一个不断调度新 Promise 的 Promise 会让循环停留在微任务阶段,定时器永远不会触发。
// ❌ 饥饿:微任务队列永远不清空
function spin() {
Promise.resolve().then(spin);
}
spin();
// ✅ 在分块之间让出事件循环
async function processInChunks(items, handle) {
for (let i = 0; i < items.length; i += 1) {
handle(items[i]);
if (i % 100 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
诊断
在 Node.js 中用 perf_hooks.monitorEventLoopDelay() 测量事件循环延迟。浏览器中的长任务会出现在 Performance 面板;输入延迟和渲染间隙是用户可见症状。CPU 分析中一个长堆栈或大量 Promise 回调通常指向原因。
何时使用分块
当任务无法移到 Worker 且必须运行在主线程时,分块执行。与 Web Worker 或 worker thread 相比,分块保留共享状态简单性,但增加总耗时。与背压相比,分块在单位工作之间让出;背压限制进入系统的工作量。
陷阱
用 setTimeout(0) 让出会增加延迟;应把工作批处理到平衡响应性和吞吐量的大小。不要假设 await 会让出到 I/O:等待已解决的 Promise 仍停留在微任务队列。应在真实负载下测试;开发环境很快的循环在生产大输入下可能饥饿。