深入理解 JavaScript 的事件循环:微任务与宏任务的精彩解析

132 阅读3分钟

JavaScript 是一种单线程语言,所有代码都在主线程上运行。为了支持异步操作,JavaScript 使用了 事件循环(Event Loop) ,通过调度 微任务(Microtasks)宏任务(Macrotasks) 实现非阻塞的异步执行。
在本篇文章中,我们将通过解析多个经典例题,逐步深入理解事件循环的工作原理和微任务、宏任务的交互机制。


什么是微任务和宏任务?

  • 微任务(Microtasks)
    微任务在当前事件循环(Event Loop)阶段内执行,优先于下一轮宏任务。
    常见例子:Promise.then()MutationObserverqueueMicrotask()
  • 宏任务(Macrotasks)
    宏任务在事件循环的下一阶段执行。每轮事件循环完成后,宏任务才开始执行。
    常见例子:setTimeoutsetIntervalsetImmediate(Node.js)。

事件循环的规则

  1. 执行同步代码(调用栈为空时停止)。
  2. 清空微任务队列(按先进先出的顺序)。
  3. 取出第一个宏任务执行,并重复步骤 1 和 2。

经典例题解析

以下通过多个经典例题展示微任务与宏任务的交替执行顺序。


例题 1: 多条 Promise 链

代码

Promise.resolve().then(() => {
    console.log(0);
    return new Promise((resolve) => { resolve(4); });
}).then((res) => {
    console.log(res);
});

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() => {
    console.log(6);
});

输出

0
1
2
3
4
5
6

解析

  1. 第一条链:Promise.resolve() 后,console.log(0) 立即执行,随后返回新的 Promise,这使得 console.log(4) 的执行被延迟到下一轮微任务队列。
  2. 第二条链:所有 .then() 按顺序注册微任务,输出 1 -> 2 -> 3 -> 5 -> 6
  3. 由于第二条链的 .then() 注册早于 console.log(4) 的微任务,因此 43 之后输出。

例题 2: 宏任务与微任务的交替

代码

console.log('start');

setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => {
    console.log('promise1');
  });
}, 0);

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

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

console.log('end');

输出

start
end
promise2
promise3
timeout1
promise1
timeout2

解析

  1. 同步代码优先执行:startend 输出。

  2. 微任务队列清空:promise2promise3

  3. 宏任务按顺序执行:

    • 第一个 setTimeout 输出 timeout1,随后执行其内的微任务 promise1
    • 第二个 setTimeout 输出 timeout2

例题 3: 微任务中嵌套宏任务

代码

console.log('start');

Promise.resolve().then(() => {
  console.log('promise1');
  setTimeout(() => {
    console.log('timeout1');
  }, 0);
}).then(() => {
  console.log('promise2');
});

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

console.log('end');

输出

start
end
promise1
promise2
timeout2
timeout1

解析

  1. 同步代码先执行:startend

  2. 微任务队列:

    • promise1 输出,注册 timeout1
    • promise2 输出。
  3. 宏任务队列:

    • timeout2 输出。
    • timeout1 在下一轮宏任务中执行。

例题 4: 动态注册微任务

代码

Promise.resolve().then(() => {
  console.log('A');
  return Promise.resolve('B').then(res => {
    console.log(res);
  });
}).then(() => {
  console.log('C');
});

输出

A
B
C

解析

  1. Promise.resolve().then() 执行,输出 A
  2. 嵌套的 Promise.resolve('B') 注册新的微任务,输出 B
  3. 外层链的下一个 .then() 被延迟到嵌套任务完成后执行,输出 C

例题 5: 微任务和宏任务的复杂交互

代码

setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => {
    console.log('promise1');
  }).then(() => {
    console.log('promise2');
  });
}, 0);

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

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

输出

promise3
promise4
timeout1
promise1
promise2
timeout2

解析

  1. 微任务队列:

    • promise3 输出。
    • promise4 紧随其后。
  2. 宏任务队列:

    • timeout1 输出,执行其内的 promise1promise2 微任务。
    • timeout2 最后输出。

学习要点

  1. 微任务优先级高于宏任务。
  2. 微任务队列遵循先进先出(FIFO)原则。
  3. 宏任务之间的间隔会清空微任务队列。