JavaScript的事件循环(Event Loop)机制

Event Loop的练习题,涵盖宏任务(macrotask)和微任务(microtask)的执行顺序,包括setTimeout、Promise、async/await等。每个题目都会给出详细解析。

题目1:基础宏任务与微任务

console.log('1');
setTimeout(() => {
  console.log('2');
}, 0);
Promise.resolve().then(() => {
  console.log('3');
});
console.log('4');

<details>
<summary>答案与解析</summary>
输出顺序:1, 4, 3, 2
解析:

  • 同步代码:先输出1和4。
  • 微任务:Promise.then是微任务,输出3。
  • 宏任务:setTimeout是宏任务,最后输出2。
    </details>

题目2:嵌套的微任务

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

<details>
<summary>答案与解析</summary>
输出顺序:start, end, promise1, promise2, timeout
解析:

  • 同步代码:输出start和end。
  • 微任务:第一个Promise.then输出promise1,并在其内部又产生一个微任务(输出promise2)。微任务队列会一直清空,所以promise2紧接着输出。
  • 宏任务:最后输出timeout。
    </details>

题目3:async函数与Promise

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
async1();
new Promise(resolve => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});
console.log('script end');

<details>
<summary>答案与解析</summary>
输出顺序:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
解析:

  • 同步代码:依次输出'script start'、'async1 start'(调用async1)、'async2'(调用async2)、'promise1'(Promise构造函数是同步的)、'script end'。
  • 微任务:await后面的代码相当于then回调,所以'async1 end'是微任务。Promise的then中'promise2'也是微任务。注意微任务队列顺序:先入队的先执行,所以先输出'async1 end'再输出'promise2'。
  • 宏任务:最后输出setTimeout。
    </details>

题目4:复杂的微任务链

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
  console.log('3');
  setTimeout(() => console.log('4'), 0);
  return 5;
}).then(value => console.log(value));
Promise.resolve().then(() => console.log('6'));
console.log('7');

<details>
<summary>答案与解析</summary>
输出顺序:1, 7, 3, 6, 5, 2, 4
解析:

  • 同步代码:1和7。
  • 微任务队列:
    • 第一个Promise.then:输出3,将setTimeout(输出4)加入宏任务队列,然后返回5,接着下一个then(输出5)加入微任务队列。
    • 第二个Promise.then:输出6。
      执行顺序:先输出3和6(因为第一个then执行后,第二个then已经在队列中,然后第一个then返回后又产生一个微任务(输出5)放在队列后面。所以微任务执行顺序是:3(同时加入新的微任务5和宏任务4)、6、5。
  • 宏任务:先输出2(第一个setTimeout),再输出4(第二个setTimeout)。
    </details>

题目5:混合事件循环阶段(Node.js环境)

注意:Node.js的事件循环阶段略有不同,但微任务依然在每一个阶段之间执行。

setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('start');

<details>
<summary>答案与解析(Node.js环境)</summary>
输出顺序:start, nextTick, Promise, setTimeout, setImmediate (或者setTimeout和setImmediate顺序可能互换)
解析:

  • 同步代码:输出start。
  • nextTick队列:nextTick回调优先级最高,输出nextTick。
  • 微任务队列:Promise.then输出Promise。
  • 宏任务:
    • setTimeout在timers阶段执行,输出setTimeout。
    • setImmediate在check阶段执行,输出setImmediate。
      注意:由于setTimeout的延迟0实际上会被设置为1ms,所以如果事件循环准备时间超过1ms,则setTimeout先执行;否则可能setImmediate先执行。但两者顺序不确定。
      </details>

题目6:async函数中的多个await

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 middle');
  await async3();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
async function async3() {
  console.log('async3');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise(resolve => {
  console.log('promise1');
  resolve();
}).then(() => console.log('promise2'));
console.log('script end');

<details>
<summary>答案与解析</summary>
输出顺序:
script start
async1 start
async2
promise1
script end
async1 middle
async3
promise2
async1 end
setTimeout
解析:

  • 同步代码:script start, async1 start, async2, promise1, script end。
  • 微任务队列:
    • 第一个await(async2)后面的代码(输出async1 middle)加入微任务队列。
    • Promise的then(输出promise2)加入微任务队列。
      执行微任务:
      • 第一个微任务:输出async1 middle,然后调用async3(输出async3,同步),async3后面的await将其后面的代码(输出async1 end)加入微任务队列。
      • 第二个微任务:输出promise2。
      • 第三个微任务:输出async1 end。
  • 宏任务:setTimeout最后输出。
    </details>

通过练习这些题目,可以加深对事件循环、微任务和宏任务的理解。在分析时,始终记住:

  1. 同步代码立即执行。
  2. 微任务(包括Promise.then、await后面的代码、process.nextTick)在当前任务结束后立即执行,且会清空整个微任务队列。
  3. 宏任务(setTimeout、setInterval、setImmediate、I/O等)在下一轮事件循环中执行。
    下面我再提供一些精心设计的 Event Loop 练习题,涵盖不同场景和复杂度,帮助你深入理解 JavaScript 的异步执行机制。每个题目都有详细解析:

题目 1:基础宏任务与微任务

console.log('1');

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

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

console.log('4');

<details> <summary>查看答案与解析</summary> 输出顺序: 1 4 3 2
解析:

同步代码:1 → 4

微任务:3

宏任务:2

</details>
题目 2:多层微任务嵌套

console.log('Start');

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

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

console.log('End');

<details> <summary>查看答案与解析</summary> 输出顺序: Start End Promise 1 Promise 2 Promise 3 Timeout 1
解析:

同步代码:Start → End

微任务队列:

执行外层 Promise:Promise 1

产生新微任务:Promise 2

再产生新微任务:Promise 3

微任务队列会完全清空

宏任务:Timeout 1

</details>
题目 3:async/await 与 Promise 混合

async function foo() {
  console.log('A');
  await bar();
  console.log('B');
}

async function bar() {
  console.log('C');
  return new Promise(resolve => {
    console.log('D');
    resolve();
  });
}

console.log('E');

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

foo();

new Promise(resolve => {
  console.log('G');
  resolve();
}).then(() => console.log('H'));

console.log('I');

<details> <summary>查看答案与解析</summary> 输出顺序: E A C D G I H B F
解析:

同步代码:E → A → C → D → G → I

微任务队列:

先执行 Promise 的 then:H

然后执行 await 后续:B

宏任务:F

</details>
题目 4:复杂微任务链

console.log('Script start');

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

Promise.resolve()
  .then(() => {
    console.log('Promise 1');
    return 'A';
  })
  .then(data => {
    console.log('Promise 2', data);
    return new Promise(resolve => {
      setTimeout(() => {
        console.log('Inner setTimeout');
        resolve('B');
      }, 0);
    });
  })
  .then(data => console.log('Promise 3', data));

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

console.log('Script end');

<details> <summary>查看答案与解析</summary> 输出顺序: Script start Script end Promise 1 Promise 4 Promise 2 A Promise 5 setTimeout Inner setTimeout Promise 3 B
解析关键点:

同步代码先执行

微任务按入队顺序执行:

第一个 Promise 链:Promise 1 → Promise 2

第二个 Promise 链:Promise 4 → Promise 5

Promise 2 中的 new Promise 包含 setTimeout:

创建新的宏任务

该宏任务完成后触发 resolve,将 Promise 3 加入微任务队列

执行顺序:

先执行第一个宏任务中的同步和微任务

然后执行 setTimeout 宏任务

再执行 Inner setTimeout 宏任务

最后执行它触发的微任务 Promise 3

</details>
题目 5:事件循环综合挑战

console.log('Main start');

document.addEventListener('click', () => {
  console.log('Click event');
  Promise.resolve().then(() => console.log('Microtask in click'));
});

setTimeout(() => {
  console.log('Timeout 1');
  Promise.resolve().then(() => {
    console.log('Microtask in timeout');
    setTimeout(() => console.log('Nested timeout'), 0);
  });
}, 100);

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

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

queueMicrotask(() => console.log('Queue microtask'));

console.log('Main end');

// 假设这里用户点击了页面

<details> <summary>查看答案与解析</summary> 输出顺序: Main start Main end Promise 1 Queue microtask Timeout 2 Click event Microtask in click Timeout 1 Microtask in timeout Timeout from promise Nested timeout
解析:

同步代码:Main start → Main end

微任务:

Promise 1

Queue microtask

宏任务:

Timeout 2(0ms 定时器)

用户交互(点击)作为宏任务:

Click event

点击中的微任务:Microtask in click

100ms 定时器宏任务:

Timeout 1

其微任务:Microtask in timeout

该微任务中创建新宏任务:Nested timeout

来自 Promise 的宏任务:Timeout from promise

执行顺序:

所有微任务在各自宏任务后立即执行

宏任务按触发顺序执行(注意定时器时间)

</details>
题目 6:async 函数中的错误处理

async function async1() {
  console.log('A');
  try {
    await async2();
  } catch (e) {
    console.log('Error:', e);
  }
  console.log('B');
}

async function async2() {
  console.log('C');
  throw new Error('Oops');
  console.log('D'); // 不会执行
}

console.log('E');

async1();

new Promise(resolve => {
  console.log('F');
  resolve();
}).then(() => console.log('G'));

console.log('H');

<details> <summary>查看答案与解析</summary> 输出顺序: E A C F H Error: Error: Oops B G
解析:

同步代码:E → A → C(执行 async2 抛出错误)→ F → H

错误处理:

await 会捕获异步错误

错误被包装为 rejected Promise

微任务队列:

首先执行错误处理:Error: Oops

然后继续 async1:B

最后执行 Promise 的 then:G

</details>
解题技巧总结:
同步代码优先:总是先执行所有同步代码

微任务队列:

在每个宏任务之后执行

完全清空(包括嵌套产生的)

来源:Promise.then/catch/finally、queueMicrotask、MutationObserver

宏任务队列:

一次事件循环执行一个

来源:setTimeout/setInterval、I/O、UI 渲染、事件回调

async/await:

await 之前的代码是同步的

await 之后的代码相当于微任务

错误处理:

async 函数中,throw 会被转换为 rejected Promise

可以使用 try/catch 捕获

建议你:

先尝试自己分析执行顺序

画出示意图(调用栈、微任务队列、宏任务队列)

使用浏览器的开发者工具调试验证

理解每个异步操作的来源和类型

这些题目覆盖了大多数 Event Loop 的考察点,掌握后你就能应对各种异步场景的分析了!

在JavaScript的事件循环(Event Loop)机制中,任务被分为两大类:宏任务(macrotask)和微任务(microtask)。它们在事件循环中的执行顺序不同:每次事件循环中,先执行一个宏任务,然后执行当前微任务队列中的所有微任务(直到清空),接着可能进行渲染,再开始下一个宏任务。

宏任务(macrotask)包括:

  1. setTimeout 和 setInterval:定时器的回调函数。
  2. I/O 操作:如文件读写、网络请求(Ajax)等,在Node.js中常见。
  3. 事件回调:如DOM事件(点击、滚动等)的回调函数。
  4. setImmediate:Node.js特有的方法,用于在当前事件循环结束后立即执行的回调。
  5. requestAnimationFrame:浏览器环境下用于在下一次重绘之前执行动画的回调(尽管它有时被单独分类,但通常被视为宏任务)。
  6. UI rendering:浏览器中的UI渲染(有些实现将其视为宏任务的一部分)。
  7. MessageChannel:用于消息传递的API,其回调属于宏任务。

微任务(microtask)包括:

  1. Promise 的回调:如 thencatchfinally 方法中的回调。
  2. MutationObserver:用于监听DOM变化的回调。
  3. queueMicrotask:专门用于将回调加入微任务队列的方法。
  4. process.nextTick:Node.js中特有的方法,它会在当前执行栈的末尾、事件循环之前执行(优先级高于其他微任务,但严格来说不属于微任务队列,而是有自己独立的队列,但行为类似微任务)。

事件循环的基本顺序:

  1. 执行一个宏任务(例如,从宏任务队列中取出一个任务执行)。
  2. 执行过程中如果遇到微任务,将其加入微任务队列。
  3. 宏任务执行完毕后,立即依次执行微任务队列中的所有微任务(直到微任务队列为空)。
  4. 如果有必要,进行UI渲染。
  5. 开始下一个宏任务。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容