Featured image of post JavaScript 事件循环深入探讨:微任务、宏任务及其他Featured image of post JavaScript 事件循环深入探讨:微任务、宏任务及其他

JavaScript 事件循环深入探讨:微任务、宏任务及其他

深入研究 JavaScript 事件循环,涵盖调用堆栈、微任务/宏任务队列、requestAnimationFrame、渲染管道、异步/等待内部结构和性能影响。

JavaScript 事件循环是该语言中最重要但又被误解的概念之一。尽管 JavaScript 是单线程的,但它的事件循环通过复杂的队列系统实现了非阻塞并发。本文构建了 JavaScript 如何处理异步执行的完整心智模型,从调用堆栈到微任务队列以及其间的所有内容。

调用堆栈和执行模型

JavaScript 使用具有 LIFO(后进先出)结构的调用堆栈。当一个函数被调用时,一个新的栈帧被压入栈中。当函数返回时,框架就会弹出。关键原则是运行到完成:每个函数在任何其他代码可以中断它之前运行完成。

function multiply(a, b) { return a * b; }
function square(n) { return multiply(n, n); }
function logSquare(x) { console.log(square(x)); }

logSquare(4); // call stack: logSquare → square → multiply → ...

这种单线程模型意味着长时间运行的操作会阻塞主线程。事件循环通过将等待操作委托给运行时环境(浏览器或 Node.js)并通过专门的队列异步处理它们的回调来解决这个问题。


宏任务队列

宏任务,也称为任务,源自 Node.js 中的 setTimeoutsetInterval、I/O 事件、UI 事件和 setImmediate。事件循环在每次迭代中从队列中选取一个宏任务,执行它直至完成,然后在移动到下一个宏任务之前处理微任务队列。

console.log("Start");
setTimeout(() => console.log("Timeout"), 0);
console.log("End");
// Output: Start, End, Timeout

即使延迟为 0,setTimeout 回调也会作为宏任务排队,并且仅在当前同步代码和所有微任务完成后才执行。这种行为经常被希望 setTimeout(fn, 0) 立即执行的开发人员误解。


微任务队列

微任务比宏任务具有更高的优先级。它们源自 Node.js 中的 Promise .then().catch().finally()queueMicrotask()MutationObserverprocess.nextTick()。关键规则是在处理下一个宏任务之前,整个微任务队列都会被清空。

console.log("1");
Promise.resolve().then(() => console.log("2"));
setTimeout(() => console.log("3"), 0);
console.log("4");
// Output: 1, 4, 2, 3

这种顺序解释了为什么 Promise 在 setTimeout 回调之前解析,即使两者都准备好了。一个危险的后果是微任务饥饿:递归调度微任务可能会阻止宏任务执行,从而冻结应用程序。

队列类型来源优先每个周期排空
宏任务setTimeout、I/O、UI 事件降低一项任务
微任务Promise、queueMicrotask更高整个队列
渲染请求动画帧之间涂漆前

异步/等待内部结构

asyncawait 关键字是 Promise 和生成器函数的语法糖。异步函数同步执行到第一个 await 表达式,此时它将控制权交还给事件循环,并在等待的 Promise 解析为微任务时恢复。

async function example() {
  console.log("A");
  await Promise.resolve();
  console.log("B");
}
example();
console.log("C");
// Output: A, C, B

await 关键字不会阻塞线程。它脱糖为 Promise .then() 回调,该回调作为微任务排队。这就是为什么 await 之后的代码在所有同步代码完成后执行。


requestAnimationFrame 和渲染

渲染管道(样式重新计算、布局和绘制)发生在事件循环周期中的特定点。 requestAnimationFrame 回调在浏览器执行绘制之前运行,使它们成为视觉更新的正确 API。

function animate(timestamp) {
  element.style.transform = `translateX(${Math.sin(timestamp / 1000) * 100}px)`;
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

对动画使用 setTimeoutsetInterval 会导致卡顿,因为它们的回调与浏览器的绘制周期不一致。另一方面,requestIdleCallback API 是为不应该影响帧预算的非关键工作而设计的。


性能影响

长任务(JavaScript 执行超过 50 毫秒)会阻塞主线程并导致明显的卡顿。 Long Tasks API 支持编程监控:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log("Long task detected:", entry.duration, "ms");
  }
});
observer.observe({ entryTypes: ["longtask"] });

长任务直接影响核心网络生命,特别是与下一次绘制的交互(INP)。缓解策略包括使用 setTimeoutscheduler.yield() 分解长任务、将繁重的计算卸载给 Web Workers,以及使用 requestIdleCallback 进行可推迟的工作。


结论

掌握事件循环对于编写高性能 JavaScript 至关重要。了解调用堆栈、宏任务队列、微任务队列和渲染管道之间的关系有助于您避免常见错误、优化动画性能并诊断生产问题。具有优先队列排出的运行到完成执行的思维模型是所有异步 JavaScript 开发的基础。