目录

前言

一、为什么需要异步? —— 单线程的困境

二、事件循环(Event Loop)的核心组件

1.调用栈(Call Stack)

2.堆(Heap)

3.任务队列(Task Queue / Callback Queue)

4.微任务队列(Microtask Queue)

执行顺序规则:

三、宏任务 vs 微任务 —— 经典面试题解析

1.基础顺序

2.嵌套微任务

四、Promise 的深入理解

1.构造函数与执行器

2. Promise 链式调用

3.静态方法

五、async/await —— 更优雅的异步语法

六、Node.js 中的事件循环差异

七、实战建议与常见陷阱

下篇总结


前言

JavaScript 是单线程的,却能高效处理 I/O 密集型任务,这得益于事件循环(Event Loop)。本篇将详细解析宏任务、微任务、Promise 内部原理以及 async/await 的语法糖本质。


一、为什么需要异步? —— 单线程的困境

假设 JavaScript 是纯粹同步的:一个网络请求需要等待 3 秒,那这 3 秒内整个页面将无法响应任何操作(UI 线程被阻塞)。为了避免这种情况,宿主环境(浏览器/Node.js)提供了异步 API(如定时器、Ajax、事件监听),并设计了一套事件循环机制来调度这些回调。

二、事件循环(Event Loop)的核心组件

事件循环本质上是一个永不停歇的循环,它的职责是监控调用栈(Call Stack)和任务队列(Task Queue),当调用栈为空时,从任务队列中取出一个任务执行。

1.调用栈(Call Stack)

  • 存储函数执行上下文的栈结构。

  • 每调用一个函数,就推入一个帧;函数返回时弹出。

  • 栈空表示当前没有正在执行的同步代码。

2.堆(Heap)

  • 用于存储对象、闭包等引用类型数据。

  • 与事件循环没有直接关系,但垃圾回收会影响性能。

3.任务队列(Task Queue / Callback Queue)

  • 存放宏任务(MacroTask) 的回调函数。

  • 常见的宏任务:setTimeoutsetIntervalI/OUI 渲染setImmediate(Node.js)。

4.微任务队列(Microtask Queue)

  • 存放微任务(MicroTask) 的回调函数,优先级高于宏任务。

  • 常见的微任务:Promise.then/catch/finallyMutationObserverqueueMicrotask

执行顺序规则

  1. 执行一个宏任务(从任务队列中取出)。

  2. 执行当前宏任务产生的所有微任务(清空微任务队列)。

  3. 可能进行 UI 渲染(浏览器)。

  4. 回到第 1 步,取出下一个宏任务。

关键点:微任务会在下一个宏任务之前全部执行完毕,因此微任务中递归添加微任务会阻塞宏任务的执行(但不会完全死循环,因为浏览器会限制递归深度)。

三、宏任务 vs 微任务 —— 经典面试题解析

1.基础顺序

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');
// 输出顺序: 1, 4, 3, 2

解析

  • 同步代码:输出 1 和 4。

  • setTimeout 回调进入宏任务队列。

  • Promise.then 回调进入微任务队列。

  • 同步代码执行完毕,调用栈空。

  • 事件循环检查微任务队列,输出 3。

  • 微任务队列清空后,取出宏任务队列中的 setTimeout 回调,输出 2。

2.嵌套微任务

Promise.resolve()
  .then(() => {
    console.log('A');
    Promise.resolve().then(() => console.log('B'));
  })
  .then(() => console.log('C'));
// 输出: A, B, C

为什么不是 A, C, B?

  • 第一个 then 回调执行时,输出 A,并向微任务队列添加了输出 B 的微任务。

  • 第一个 then 返回一个新的 Promise,它的 then 回调(输出 C)会被添加到微任务队列,但排在 B 之后。

  • 当前宏任务结束后,清空微任务队列的顺序是:先 B,后 C。

四、Promise 的深入理解

Promise 是 ES6 引入的异步编程解决方案,它本质是一个状态机,有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态一旦改变,就不可逆。

1.构造函数与执行器

const promise = new Promise((resolve, reject) => {
  // 异步操作
  if (success) resolve(value);
  else reject(error);
});

执行器函数会立即同步执行,而 then 和 catch 中的回调会作为微任务异步执行。

2. Promise 链式调用

then 方法返回一个新的 Promise,因此可以实现链式调用。链中的值会通过 return 传递。

Promise.resolve(1)
  .then(x => x + 1)
  .then(x => { throw new Error('oops'); })
  .catch(err => console.log(err.message));

3.静态方法

  • Promise.all:所有 Promise 成功则返回结果数组,有一个失败则立即失败。

  • Promise.allSettled:等待所有 Promise 完成(无论成功或失败),返回状态数组。

  • Promise.race:返回最先完成(成功或失败)的 Promise 结果。

  • Promise.any:返回第一个成功的 Promise,若全部失败则抛出 AggregateError。

五、async/await —— 更优雅的异步语法

async 函数返回一个 Promise,await 表达式会暂停当前 async 函数的执行,等待右侧 Promise 解决后恢复执行,并返回解决的值。

本质await 后面的代码相当于被包装在 Promise.then 中,因此属于微任务。

async function fetchData() {
  console.log('start');
  const data = await new Promise(resolve => setTimeout(() => resolve('data'), 1000));
  console.log(data);
  return 'done';
}
fetchData().then(console.log);
// 输出: start, (1秒后) data, done

错误处理:使用 try...catch 包裹 await,或者不捕获则返回 rejected Promise。

六、Node.js 中的事件循环差异

Node.js 的事件循环分为多个阶段:timers、pending callbacks、idle/prepare、poll、check、close。每个阶段都有一个对应的宏任务队列。

  • timers:执行 setTimeout 和 setInterval 回调。

  • poll:获取新的 I/O 事件,执行 I/O 回调。

  • check:执行 setImmediate 回调。

process.nextTick 既不是宏任务也不是微任务,它会在当前阶段结束后、下一阶段开始前执行,优先级高于微任务。

七、实战建议与常见陷阱

  • 避免长时间阻塞调用栈:大量计算可以拆分成多个宏任务(使用 setTimeout 分片)或使用 Web Worker。

  • 合理使用微任务:微任务能更快执行,但过多微任务会延迟 UI 渲染。

  • Promise 必须处理错误:总是添加 .catch 或在 async 函数中使用 try/catch

  • 循环中的异步for 循环中使用 await 会顺序执行;并发使用 Promise.all

// 顺序执行(慢)
for (const url of urls) {
  await fetch(url);
}

// 并发执行(快)
await Promise.all(urls.map(url => fetch(url)));

下篇总结

事件循环是 JavaScript 非阻塞 I/O 模型的灵魂。理解宏任务、微任务的执行顺序,能帮你轻松应对异步代码的调试和面试题。结合 Promise 和 async/await,你可以写出既清晰又高效的异步代码。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐