返回文章
JavaScript约 3 分钟阅读

从一段输出顺序,看懂事件循环

同步任务、Promise 和 setTimeout 到底谁先执行?用一个足够小的例子,把浏览器的调度过程拆开。

GUAN前端开发手记
示例文章

收藏保存在此浏览器,可在归档页查看。

文章目录4 个章节

理解事件循环,不妨从预测四行日志开始。先不运行,写下你认为的顺序。

一段最小代码

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 面板确认瓶颈,再决定采用哪一种方式。

读到这里,不妨动手试一试。
把一个概念变成能运行的小实验。

找一篇笔记

输入关键词,或浏览全部文章。

按 Tab 选择文章Esc 关闭