JS事件循环Event Loop

171 阅读4分钟

我们都知道JavaScript是一门单线程且非阻塞的脚本语言,这意味着JavaScript代码在执行的任何时候都只有一个主线程来处理所有任务。而非阻塞是指当代码需要处理异步任务时,主线程会挂起(pending)这个任务,当异步任务处理完毕后,主线程再根据一定规则去执行相应回调。

事实上,当任务处理完毕后,JavaScript会将这个事件加入一个队列中,我们称这个队列为事件队列。被放入事件队列中的事件不会立刻执行其回调,而是等待当前执行栈中的所有任务执行完毕后,主线程会去查找事件队列中是否有任务。

异步任务有两种类型:微任务(microtask)和宏任务(macrotask)。不同类型的任务会被分配到不同的任务队列中。

属于微任务的事件包括但不限于以下几种:

  • Promise.then
  • MutationObserver
  • Object.observe
  • process.nextTick

属于宏任务的事件包括但不限于以下几种:

  • setTimeout
  • setInterval
  • setImmediate
  • MessageChannel
  • requestAnimationFrame
  • I/O
  • UI交互事件


执行一个JavaScript代码的具体流程:

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(如setTimeout等);
  2. 全局Script代码执行完毕后,调用栈Stack会清空;
  3. 从微任务队列(microtask queue)中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到把microtask queue中的所有任务都执行完毕。PS,如果在执行microtask的过程中,又产生了microtask,那么会加入队列的末尾,也会在这个周期被调用执行。
  5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
  6. 取出宏任务队列(macrotask queue)中位于队首的任务,放入Stack中执行;
  7. 执行完毕后,调用栈Stack为空;
  8. 重复第3-7步骤,直到Stack queue,macrotask queue,microtask queue都为空。

以上,就是浏览器的事件循环Event Loop.

下面来看一个示例代码,测试一下理解程度:

console.log(1);

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

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

setTimeout(() => {
  console.log(6);
})

console.log(7);


大家可以自行在浏览器中执行查看结果,这里我们分析一下整个流程:

Step 1:执行全局Script代码

console.log(1);

Stack Queue: [console]

Macrotask Queue: []

Microtask Queue: []

打印结果: 1


Step 2:执行setTimeout

setTimeout(() => {
  // 下面的回调函数叫做callback1,加入macrotask queue
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

Stack Queue: [setTimeout]

Macrotask Queue: [callback1]

Microtask Queue: []

打印结果: 1


Step 3:执行Promise

new Promise((resolve, reject) => {
  // 注意这里的console.log(4)是同步执行 ⭐️⭐️⭐️
  console.log(4)
  resolve(5)
}).then((data) => {
  // 这个回调函数叫做callback2,加入microtask queue中
  console.log(data);
})

Stack Queue: [promise]

Macrotask Queue: [callback1]

Microtask Queue: [callback2]

打印结果:1    4


Step 5:执行setTimeout

setTimeout(() => {
  //这个回调函数叫做callback3,放在macrotask中
  console.log(6);
})

Stack Queue: [setTimeout]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印结果:1    4


Step 6:执行console.log

console.log(7);

Stack Queue: [console]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印结果:1    4    7


现在全局Script代码执行完啦,我们开始进入下一个步骤,从microtask queue中依次取出任务执行,直到microtask queue为空。

Step 7:执行microtask queue中的第一个任务,callback2

console.log(data);  //这里的data是Promise的resolve(5)

Stack Queue: [callback2]

Macrotask Queue: [callback1, callback3]

Microtask Queue: []

打印结果:1    4    7    5


microtask queue中只有一个任务,执行完毕,进入下一个步骤,开始从宏任务队列macrotask queue中取位于队首的任务执行

Step 8:执行callback1

console.log(2);

Stack Queue: [callback1]

Macrotask Queue: [callback3]

Microtask Queue: []

打印结果:1    4    7    5    2


Step 9:遇到promise,添加callback4回调函数

Promise.resolve().then(() => {
    // 回调函数叫做callback4,添加到microtask queue中
    console.log(3)
});

Stack Queue: [promise]

Macrotask Queue: [callback3]

Microtask Queue: [callback4]

打印结果:1    4    7    5    2


此时取出的一个宏任务macrotask执行完毕,下一个步骤,再去微任务队列microtask queue中依次取出执行

Step 10:执行callback4

console.log(3);

Stack Queue: [callback4]

Macrotask Queue: [callback3]

Microtask Queue: []

打印结果:1    4    7   5    2    3

微任务队列全部执行完,去宏任务队列macrotask queue中取队首任务执行

Step 11:执行callback3

console.log(6);

Stack Queue: [callback3]

Macrotask Queue: []

Microtask Queue: []

打印结果:1    4    7    5    2    3    6


以上,全部执行完毕,Stack Queue、macrotask queue、microtask queue均为空,循环完毕。

Stack Queue: []

Macrotask Queue: []

Microtask Queue: []

最终打印结果:1    4    7    5    2    3    6


你写对了吗?

相信通过这个示例步骤,你能明白事件循环Event Loop的原理。