关于浏览器Event Loop的两三事

194 阅读4分钟

Event Loop(事件循环),是 JavaScript 中的一个执行模型,负责执行代码、收集和处理事件以及执行队列中的子任务。事件循环很重要,它使得 JavaScript 实现单线程非阻塞成为可能。Event Loop 在不同的地方(比如浏览器和 Node.js)有不同的表现,我们这里只讨论浏览器的 Event Loop 。

宏队列和微队列

宏队列,即 macrotask queue 。一些异步任务的回调会依次进入该队列,等待被调用。这些异步任务包括:

  • setTimeout
  • setInterval
  • setImmediate(Node独有)
  • requestAnimationFrame(浏览器独有)
  • I/O
  • UI rendering(浏览器独有)
  • ......

微队列,即 microtask queue 。另一些异步任务的回调会依次进入该队列,等待被调用。这些异步任务包括:

  • Promise
  • process.nextTick(Node独有)
  • Object.observe(Node独有)
  • MutationObserver(浏览器独有)
  • ......

执行 JavaScript 代码的过程如下:

  1. 执行浏览器全局的 JavaScript 代码,这个过程就是把任务推入主流程的调用栈中。同步的立即执行,异步的推入相应的队列中等待执行(宏任务推入宏队列,微任务推入微队列);
  2. 当主流程的调用栈被清空之后,查看微队列是否为空。不为空则取出队首的回调任务,把该任务推入调用栈调用,微队列长度减一,一直重复这个过程,直到微队列为空。这个过程如果生成新的微任务,则继续推入微队列的尾部,同样在这个周期执行;
  3. 当微队列被清空之后,检测宏队列是否为空,若不为空,则取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;(只取出一次)
  4. 执行完毕之后,调用栈被清空;
  5. 一直重复上述过程......

以上就是 Event Loop ,JavaScript 事件循环的执行过程。有两个需要注意的点:

  1. 宏任务队列一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  2. 微任务队列中所有的任务都会被依次取出来执行,直到微任务队列为空。

上代码分析一下上述流程

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);

// 执行结果
1
4
7
5
2
3
6

下面简单讲解一下执行过程:

step1:

stack: [1] microtask: [] macrotask: [] output:1

console.log(1) 同步代码,推入调用栈直接调用输出,并把该任务推出调用栈

step2:

stack: [setTimeout] microtask: [] macrotask: [2->3] output:1

setTimeout 为宏队列任务,把该任务的回调推入宏队列任务中等待调用,并把该任务推出调用栈

step3:

stack: [promise] microtask: [5] macrotask: [2->3] output:1,4

promise 为微队列任务,把该任务的回调推入微队列任务中等待调用,由于 console.log(4) 在此位置是同步执行的,所以调用栈直接调用并输出4,并把该任务推出调用栈

step4:

stack: [setTimeout] microtask: [5] macrotask: [2->3, 6] output:1,4

setTimeout 为宏队列任务,把该任务的回调推入宏队列任务中等待调用,并把该任务推出调用栈

step5:

stack: [7] microtask: [5] macrotask: [2->3, 6] output:1,4,7

console.log(1) 同步代码,推入调用栈直接调用输出,并把该任务推出调用栈

step6:

stack: [] microtask: [5] macrotask: [2->3, 6] output:1,4,7,5

此时调用栈为空,接下来查看微任务队列,依次把微任务队列的队首任务取出推入调用栈,并调用输出。重复这个过程直到微任务队列为空

step7:

stack: [] microtask: [3] macrotask: [6] output:1,4,7,5,2

此时调用栈为空,microtask queue 为空。接下来查看 macrotask queue,取出宏队列首部任务推入调用栈,并调用输出,此时产生了新的微任务(3),推入微任务队列尾部。注意:宏任务每次只取一个任务。

step8:

stack: [] microtask: [] macrotask: [6] output:1,4,7,5,2,3

继续查看微任务队列,依次把微任务队列的队首任务取出推入调用栈,并调用输出。重复这个过程直到微任务队列为空

step8:

stack: [] microtask: [] macrotask: [] output:1,4,7,5,2,3,6

此时微任务队列为空,查看宏任务队列,取出宏队列首部任务推入调用栈,并调用输出。注意:宏任务每次只取一个任务。

执行完毕,调用栈为空,微队列为空,宏队列为空。

下面再看一个例子加深一下理解,具体过程我就不写出来啦,相信你们都能做出来~~

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);
  
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

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

console.log(10);

// 执行结果
1
4
10
5
6
7
2
3
9
8

浏览器的 Event Loop 执行机制大概就是这样啦~~