JavaScript是一门单线程语言,但通过事件循环(Event Loop)机制,它能够处理异步操作,实现非阻塞的I/O模型。让我们深入了解Web浏览器中的事件循环机制。

事件循环的基本概念

事件循环主要由以下几个部分组成:

  1. 调用栈(Call Stack):用于存储正在执行的函数调用。
  2. 堆(Heap):用于存储对象。
  3. 任务队列(Task Queue):
    • 宏任务队列(Macrotask Queue)
    • 微任务队列(Microtask Queue)

事件循环的流程

让我们用一个流程图来说明事件循环的工作原理:

24ce97fab081778e83241628bff7ae04.png

 

  1. 首先,执行调用栈中的同步代码。
  2. 当调用栈为空时,检查微任务队列。
  3. 执行所有微任务直到微任务队列为空。
  4. 从宏任务队列中取出一个任务执行。
  5. 重复步骤2-4,直到所有任务都执行完毕。

宏任务vs微任务

理解宏任务和微任务的区别对于掌握事件循环机制至关重要。

宏任务包括:

  • script(整体代码)
  • setTimeout/setInterval
  • UI渲染
  • I/O操作
  • postMessage
  • MessageChannel

微任务包括:

  • Promise的then/catch/finally回调
  • MutationObserver回调
  • queueMicrotask()

实例分析

让我们通过一个例子来理解事件循环的执行过程:

 1console.log('1');  
 2
 3setTimeout(() => {
 4    console.log('2'); 
 5}, 0);
 6
 7Promise.resolve().then(() => {
 8    console.log('3');  
 9});
10
11new Promise((resolve) => {
12    console.log('4');  
13    resolve();
14}).then(() => {
15    console.log('5');  
16});
17
18console.log('6'); 

执行顺序分析:

  1. 首先执行同步代码,输出:1, 4, 6
  2. 将setTimeout回调放入宏任务队列
  3. 将第一个Promise的then回调放入微任务队列
  4. 将第二个Promise的then回调放入微任务队列
  5. 同步代码执行完毕,检查微任务队列,依次执行微任务,输出:3, 5
  6. 微任务队列清空,执行下一个宏任务(setTimeout回调),输出:2

最终输出顺序:1, 4, 6, 3, 5, 2

async/await 在事件循环中的表现

首先,让我们看一个包含 async/await 的代码示例:

 1console.log('1'); 
 2
 3async function asyncFunction() {
 4    console.log('2');
 5    await Promise.resolve();
 6    console.log('3');
 7    await Promise.resolve();
 8    console.log('4');
 9}
10
11setTimeout(() => {
12    console.log('5');
13}, 0);
14
15asyncFunction();
16
17new Promise((resolve) => {
18    console.log('6');
19    resolve();
20}).then(() => {
21    console.log('7');
22});
23
24console.log('8');

让我们分析这段代码的执行顺序:

  1. 首先执行同步代码:
    • 输出 ‘1’
    • 遇到 asyncFunction() 调用,进入函数内部
    • 输出 ‘2’
    • 遇到第一个 await,将后续代码放入微任务队列,然后跳出 asyncFunction
    • setTimeout 回调放入宏任务队列
    • 执行 Promise 构造函数中的同步代码,输出 ‘6’
    • 输出 ‘8’
  2. 同步代码执行完毕,检查微任务队列:
    • 执行 asyncFunction 中第一个 await 后的代码,输出 ‘3’
    • 遇到第二个 await,将后续代码再次放入微任务队列
    • 执行 Promise 的 then 回调,输出 ‘7’
  3. 当前微任务队列执行完毕,再次检查微任务队列:
    • 执行 asyncFunction 中第二个 await 后的代码,输出 ‘4’
  4. 微任务队列清空,执行下一个宏任务:
    • 执行 setTimeout 回调,输出 ‘5’

最终输出顺序:1, 2, 6, 8, 3, 7, 4, 5

注意事项

  1. Promise构造函数中的代码是同步执行的。
  2. async/await是Promise的语法糖,await后面的代码相当于放在then方法的回调中。
  3. 每执行完一个宏任务后,都会检查并执行微任务队列中的所有任务。

结论

理解事件循环机制对于编写高效的异步JavaScript代码至关重要。通过合理安排宏任务和微任务,我们可以优化代码执行顺序,提高应用性能。在实际开发中,要注意区分同步任务、宏任务和微任务,以便更好地控制代码流程。下一篇我们一起看一下Node.js事件循环机制。

个人笔记记录 2021 ~ 2025