文章目录

JavaScript 事件循环:宏任务与微任务

发布于 2026-04-07 23:28:20 · 浏览 9 次 · 评论 0 条

JavaScript 事件循环:宏任务与微任务

JavaScript 是单线程语言,这意味着它一次只能做一件事。为了不阻塞主线程(导致页面卡顿),JavaScript 采用了一种“事件循环”机制来处理异步操作。理解这一机制的关键,在于分清“宏任务”和“微任务”的区别与执行顺序。

以下指南将直接解析核心概念,并通过代码演练帮你彻底掌握这一运行机制。


一、 核心概念:任务队列

浏览器或 Node.js 在执行代码时,会将任务分类存放在不同的队列中。你需要先搞清楚这两个队列里分别装什么。

任务类型 包含内容 常见 API
宏任务 耗时较长的操作,脚本级别的代码 script (整体代码), setTimeout, setInterval, setImmediate (Node.js), I/O 操作
微任务 耗时较短,需要高优先级执行的操作 Promise.then/catch/finally, process.nextTick (Node.js), MutationObserver

二、 执行流程:事件循环的运作逻辑

事件循环是一个无限循环的过程,它决定了代码执行的先后顺序。请按照以下逻辑步骤理解引擎的工作方式:

  1. 执行同步代码
    引擎首先从头到尾执行当前的“主脚本”(这也是一个宏任务)。所有同步代码直接运行,遇到异步任务时,将其交给相应的 Web API 处理,并将回调函数注册到对应的任务队列中。

  2. 检查微任务队列
    同步代码执行完毕后,引擎立即检查微任务队列。如果队列中有任务,一次性执行队列中的所有微任务,直到队列被清空。在此期间,如果有新的微任务产生,也会立即加入当前队列并继续执行。

  3. 执行 UI 渲染
    当微任务队列完全清空后,浏览器通常会进行页面渲染(如果需要重绘或回流)。

  4. 执行一个宏任务
    渲染完成后,引擎从宏任务队列中取出一个任务放入执行栈执行。

  5. 循环回到步骤 2
    宏任务执行完毕后,再次检查微任务队列。这个“宏任务 -> 微任务 -> 渲染 -> 宏任务”的过程不断循环。

为了更直观地展示这一流程,请参考下方的逻辑图:

graph TD Start[开始] --> RunSync["执行同步代码 (主宏任务)"] RunSync --> CheckMicro{"微任务队列\n是否有任务?"} CheckMicro -- 是 --> RunAllMicro["执行所有微任务\n直至清空"] RunAllMicro --> CheckMicro CheckMicro -- 否 --> Render["UI 渲染 (可选)"] Render --> CheckMacro{"宏任务队列\n是否有任务?"} CheckMacro -- 是 --> RunOneMacro["执行一个宏任务"] RunOneMacro --> CheckMicro CheckMacro -- 否 --> Wait["等待新任务"] Wait --> CheckMacro

三、 实战演练:代码执行顺序推演

通过具体的代码案例,我们将上述理论转化为可预测的输出结果。

案例一:基础顺序测试

请看以下代码:

console.log('1. 开始');

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

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

console.log('4. 结束');

推演步骤:

  1. 执行同步代码:
    • 遇到 console.log('1. 开始')输出 1. 开始
    • 遇到 setTimeout,将其回调放入宏任务队列。
    • 遇到 Promise.resolve().then,将其回调放入微任务队列。
    • 遇到 console.log('4. 结束')输出 4. 结束
  2. 检查微任务队列:
    • 队列中有 Promise.then 的回调。
    • 执行回调,输出 3. Promise.then
  3. 执行宏任务队列:
    • 队列中有 setTimeout 的回调。
    • 执行回调,输出 2. setTimeout

最终控制台输出结果:

1. 开始
4. 结束
3. Promise.then
2. setTimeout

案例二:嵌套任务与微任务的“陷阱”

这是一个更容易出错的复杂案例:

console.log('Script Start');

setTimeout(() => {
  console.log('Timeout 1');
  Promise.resolve().then(() => {
    console.log('Promise inside Timeout');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
  setTimeout(() => {
    console.log('Timeout inside Promise');
  }, 0);
});

console.log('Script End');

推演步骤:

  1. 执行同步代码:
    • 输出 Script Start
    • setTimeout 回调 -> 宏任务队列(记为 宏1)。
    • Promise.then 回调 -> 微任务队列(记为 微1)。
    • 输出 Script End
  2. 检查微任务队列:
    • 执行 微1 (Promise 1)。
    • 在微1内部,遇到 setTimeout,将其回调放入宏任务队列(记为 宏2)。
    • 微任务队列此时已清空。
  3. 执行宏任务队列:
    • 执行 宏1 (Timeout 1)。
    • 在宏1内部,遇到 Promise.then,将其回调放入微任务队列(记为 微2)。
    • 宏1执行完毕。
  4. 检查微任务队列(关键点:每次宏任务执行完后都要检查微任务):
    • 队列中有 微2 (Promise inside Timeout)。
    • 执行 微2,输出 Promise inside Timeout
  5. 执行宏任务队列:
    • 执行 宏2 (Timeout inside Promise)。
    • 输出 Timeout inside Promise

最终控制台输出结果:

Script Start
Script End
Promise 1
Timeout 1
Promise inside Timeout
Timeout inside Promise

四、 关键点总结与避坑

为了确保你编写的异步代码逻辑正确,请务必遵守以下规则:

  1. 记住优先级
    微任务的优先级永远高于宏任务。只要微任务队列中还有任务,事件循环就会一直处理微任务,而不会去处理宏任务。

  2. 区分 Promise 的状态变化
    new Promise((resolve) => {...}) 构造函数中的代码是同步执行的。只有 resolve() 后, .then() 中的回调才会进入微任务队列。

    console.log('A');
    new Promise((resolve) => {
      console.log('B'); // 同步执行
      resolve();
    }).then(() => {
      console.log('C'); // 微任务
    });
    console.log('D');
    // 输出顺序: A, B, D, C
  3. 警惕宏任务中的微任务嵌套
    如案例二所示,当一个宏任务正在执行时,它产生的微任务不会立即执行,必须等待当前宏任务执行完毕,事件循环回到“检查微任务”阶段时才会执行。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文