深入理解 JS —— 事件循环

什么是事件循环?

JavaScript 是单线程的语言,意味着它一次只能做一件事。但现代 Web 应用往往需要处理大量的异步操作,比如 DOM 事件处理、网络请求、定时器等。为了避免阻塞主线程,JavaScript 使用了事件循环机制来实现异步操作。

事件循环的核心是一个不断轮询的过程,用来检查是否有待处理的任务。在任务队列中有任务时,事件循环将它们取出并执行。

JavaScript 执行模型

JavaScript 的执行模型通常由以下几个部分组成:

  1. 调用栈(Call Stack) 调用栈是一个栈结构,用于存储当前正在执行的函数。当你调用一个函数时,它被压入栈顶,函数执行完毕后从栈顶弹出。
  2. 消息队列(Message Queue) 消息队列存储的是待执行的异步任务。比如,定时器回调、网络请求的响应等。
  3. 事件循环(Event Loop) 事件循环负责检查调用栈是否为空。如果栈为空,它就会检查消息队列中是否有待执行的任务。如果队列中有任务,事件循环就会将这些任务依次取出并压入调用栈执行。
  4. Web APIs(浏览器环境) Web APIs 是浏览器提供的异步 API,比如 setTimeoutfetch 等。它们会在调用时将任务交给浏览器去处理,一旦处理完成,回调函数会被推送到消息队列中等待事件循环的执行。

事件循环的执行流程

简单来说,JavaScript 的事件循环过程可以分为以下几个步骤:

  1. 执行同步代码,压入调用栈。
  2. 当调用栈清空后,事件循环会检查消息队列中是否有待执行的任务。
  3. 如果有任务,事件循环会将这些任务逐个取出并执行。

具体流程

  • 执行同步任务:首先,JavaScript 引擎从全局执行上下文开始,执行同步任务。同步任务会立即压入调用栈,依次执行。
  • 执行异步任务:当遇到异步任务(如 setTimeoutfetch 等),这些任务会被交给浏览器的 Web APIs 处理。当这些任务完成后,回调函数会被推送到消息队列中,等待事件循环的轮询。
  • 事件循环轮询:当调用栈为空时,事件循环开始执行消息队列中的任务。它会取出队列中的任务,压入调用栈,并执行它们。

微任务与宏任务

JavaScript 中的异步任务分为两类:宏任务(Macrotask)和微任务(Microtask)。事件循环会先执行完所有宏任务,然后再执行微任务。微任务的优先级高于宏任务。

宏任务(Macrotask)微任务(Microtask)
DOM 事件(如点击、输入框变化等)Promise.then
I/O 操作(如文件读写)async / await
网络请求(Ajax)

执行顺序

  1. 执行同步代码(当前宏任务)。
  2. 执行所有微任务(微任务队列)。
  3. 执行下一个宏任务,重复上述步骤。

这种机制保证了微任务比宏任务优先执行,且所有微任务都会在每个宏任务后执行完毕。

易错点

  1. Promise 本身是一个同步的代码(只是容器),只有它后面调用的 then() 方法里面的回调才是微任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    new Promise(resolve => {
      console.log(1);
      resolve();
    }).then(() => {
      console.log(2);
    });
    console.log(3);
    
    // 1 3 2
  2. await 右边的表达式还是会立即执行,表达式之后的代码才是微任务,await 微任务可以转换成等价的 Promise 微任务分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    console.log(1);
    
    async function async1() {
      await async2();
      console.log(2);
    }
    
    async function async2() {
      console.log(3);
    }
    
    async1();

面试题

一.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log(1);
async function async1() {
  console.log(2);
  await async2();
  console.log(3);
  await async3();
  console.log(4);
}
async function async2() {
  console.log(5);
}
async function async3() {
  console.log(6);
}
async1();
console.log(7);

// 1 2 5 7 3 6 4
  1. 先执行同步代码 console.log(1)
  2. 然后执行异步函数 async1()
    1. 执行遇到第一个 await 之前的代码 console.log(2)
    2. 执行 async2() 中的 console.log(5)
    3. 将第一个 await 之后的代码放入微任务队列
  3. 继续执行同步代码 console.log(7)
  4. 同步代码结束,执行微任务
    1. console.log(3)
    2. 执行 await async3() ,完毕后将 console.log(4) 放入微任务队列
    3. 执行 console.log(4)

二.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log(1);

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

new Promise(resolve => {
  console.log(6);
  resolve();
}).then(() => {
  console.log(7);
});

console.log(8);

// 1 6 8 7 2 3 5 4
  1. 先执行同步代码,输出 \(1, 6, 8\)Promise 是同步的,then 的回调才是微任务)
  2. setTimeout() 是宏任务,放入宏任务队列
  3. 执行微任务 console.log(7)
  4. 执行宏任务队列中的 \(2, 3, 5\) ,将 then() 中的 console.log(4) 放入微任务
  5. 执行微任务 console.log(4)

三.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
console.log(1);

async function async1() {
  console.log(2);
  await async2();
  console.log(3);
  await async3();
  console.log(4);
}

async function async2() {
  console.log(5);
}

async function async3() {
  console.log(6);
}

console.log(7);

async1();

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

new Promise(resolve => {
  console.log(9);
  resolve();
})
  .then(() => {
    console.log(10);
  })
  .then(() => {
    console.log(11);
  });

console.log(12);

// 1 7 2 5 9 12 3 6 10 4 11 8
  1. 先执行同步代码,输出 \(1,7\)
  2. 遇到异步函数 async1()
    1. 执行第一个 await 之前的 console.log(2)
    2. 执行 async2(),输出 \(5\)
    3. 将第一个 await 之后的代码放入微任务队列
  3. setTimeout() 的回调函数放入宏任务队列
  4. 执行同步任务 Promise 中的 console.log(9),将第一个 then() 的回调函数放入微任务队列
  5. 执行同步任务 console.log(12)
  6. 执行微任务队列中的 console.log(3)async3(),将 console.log(4) 放入微任务队列
  7. 继续执行微任务队列中的 console.log(10),将下一个 then()的回调函数 console.log(11) 放入微任务队列
  8. 继续执行微任务队列中剩下的任务,输出 \(4, 11\),微任务队列清空
  9. 开始执行宏任务队列中的 console.log(8)

四.

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

// 1 2 4 3 7 6 9 5 8
  1. 执行同步代码
    1. 执行 console.log(1) ,输出 \(1\)
    2. 执行 console.log(2) ,输出 \(2\)
    3. 将内部 setTimeout 的回调放入宏任务队列(记为宏任务 \(1\)
    4. 将外部 setTimeout 的回调放入宏任务队列(记为宏任务 \(2\)
  2. 微任务队列为空,处理宏任务队列,执行宏任务 \(1\) (内部 setTimeout
    1. then() 的回调放入微任务队列(记为微任务 \(1\)
    2. 执行 console.log(4) ,输出 \(4\)
  3. 宏任务 \(1\) 处理完毕,检查微任务队列,不为空,处理微任务队列,执行微任务 \(1\)
    1. setTimeout 的回调放入宏任务队列(记为宏任务 \(3\)
    2. 执行 console.log(data),输出 \(3\)
  4. 微任务队列为空,处理宏任务队列,执行宏任务 \(2\) (外部 setTimeout
    1. then() 的回调放入微任务队列(记为微任务 \(2\)
    2. 执行 console.log(7) ,输出 \(7\)
  5. 宏任务 \(2\) 处理完毕,检查微任务队列,不为空,处理微任务队列,执行微任务 \(2\)
    1. 执行 console.log(data),输出 \(6\)
    2. setTimeout 的回调放入宏任务队列(记为宏任务 \(4\)
    3. 执行 console.log(9) ,输出 \(9\)
  6. 微任务队列为空,处理宏任务队列,执行宏任务 \(3\) (微任务 \(1\)setTimeout
    1. 执行 console.log(5),输出 \(5\)
  7. 微任务队列为空,处理宏任务队列,执行宏任务 \(4\) (微任务 \(2\)setTimeout
    1. 执行 console.log(8),输出 \(8\)

附录

参考资料

  1. 你知道JS的执行原理吗?一文详解Event Loop事件循环、微任务、宏任务 - 掘金

深入理解 JS —— 事件循环
http://xiaowhang.github.io/archives/1540962542/
作者
Xiaowhang
发布于
2025年2月21日
许可协议