理解事件循环,不妨从预测四行日志开始。先不运行,写下你认为的顺序。
一段最小代码
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');在普通浏览器脚本中,输出是 A → D → C → B。同步代码先执行,Promise 回调进入微任务队列,定时器回调等待后续任务执行的机会。
微任务发生在什么时候
浏览器完成当前任务,在相应的微任务检查点处理微任务。上面的脚本先打印 A 和 D,然后处理 Promise 回调,最后才轮到定时器。
微任务执行时新加入的微任务,也会在同一轮检查中继续处理。因此,不断递归创建微任务可能阻止浏览器及时响应输入或绘制页面。
// 有限的示例;不要在这里创建无限递归。
queueMicrotask(() => {
console.log('microtask 1');
queueMicrotask(() => console.log('microtask 2'));
});零毫秒不是立刻执行
setTimeout(fn, 0) 表示等待时间的下限,不是准时承诺。当前 JavaScript 工作、浏览器调度、嵌套定时器限制以及后台标签页节流,都可能推迟回调。
如果要更新与下一帧有关的视觉状态,可以考虑 requestAnimationFrame。它在浏览器准备更新画面时调用回调,但也不意味着任何任务都适合塞进这一帧。
长任务要真正让出线程
把同步计算包进 Promise,不会自动把计算搬到后台。await Promise.resolve() 通常也只是让出到微任务,不能保证浏览器获得绘制机会。
处理大量数据时,可以拆成小批次,通过任务调度让出主线程;对于独立、耗时的计算,可以使用 Web Worker。先在 Performance 面板确认瓶颈,再决定采用哪一种方式。
读到这里,不妨动手试一试。
把一个概念变成能运行的小实验。