EventLoop事件循环

86 阅读14分钟

一、概论

  1. JS是单线程的,即JS中的代码都是串行的,前面没有执行完毕后面不能执行。
  2. JS程序的执行顺序按照以下的原则:
    • 程序运行会从上至下依次执行所有的同步代码
    • 在执行的过程中如果遇到异步代码会将异步代码放到事件循环中
    • 当所有的同步代码都执行完毕后,JS会不断检测事件循环中的异步代码是否满足执行条件,一旦满足条件就会执行满足条件的异步代码
  3. 宏任务和微任务
    • 在JS异步代码中又区分“宏任务MacroTast”和“微任务(MicroTask)”
    • 常见的宏任务有:setTimeout、setInterval、setImmediate(IE独有)、... ...
    • 常见的微任务有:Promise、MutationObserver、process.nextTick(node独有)、... ...
  4. 宏任务和微任务的执行顺序:
    • 宏任务和微任务都有自己的任务队列
    • 所有的宏任务和微任务都会放到自己的执行队列中去
    • 所有放到队列中的任务都采用“先进先出”的原则,就是说多个任务同时满足执行条件,那么就会先执行先放进去的
    • 每执行完一个宏任务都会立刻检查微任务队列有没有被清空,如果没有就立刻清空
  5. 完整的执行顺序
    • 从上至下执行所有的同步代码
    • 在执行过程中遇到宏任务就放到宏任务队列中,遇到微任务就放到微任务队列中
    • 当所有同步代码执行完毕后,就执行微任务队列中满足需求的所有回调
    • 当微任务队列所有满足需求回调执行完毕之后,就执行宏任务队列中满足需求的所有回调
    • 每执行完一个宏任务都会立刻检查微任务队列有没有被清空, 如果没有就立刻清空
    • ... ...

二、解析

  • 代码:
// 定义一个宏任务s1
setTimeout(function () {
   console.log("s1开始");
   // 定义一个微任务p4
   Promise.resolve().then(function () {
       console.log("p4");
   });
   // 定义一个宏任务s4
   setTimeout(function () {
       console.log("s4");
   });
   // 定义一个微任务p5
   Promise.resolve().then(function () {
       console.log("p5");
   });
   console.log("s1结束");
}, 0);
// 定义一个微任务p1
Promise.resolve().then(function () {
   console.log("p1");
});
console.log("同步代码Start");
// 定义一个微任务p2
Promise.resolve().then(function () {
   console.log("p2");
   // 定义一个宏任务s3
   setTimeout(function () {
       console.log("s3");
   });
   // 定义一个微任务p3
   Promise.resolve().then(function () {
       console.log("p3");
   });
});
// 定义一个宏任务s2
setTimeout(function () {
   console.log("s2");
   // 定义一个宏任务s5
   setTimeout(function () {
       console.log("s5");
   });
   // 定义一个微任务p6
   Promise.resolve().then(function () {
       console.log("p6");
   });
}, 0);
console.log("同步代码End");
  • 打印结果:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5、s2、p6、s3、s4、s5

  • 执行顺序分析:

  1. 浏览器从上至下解析代码
  2. 先遇到s1,s1是宏任务,遵循原则:在执行过程中遇到宏任务就放到宏任务队列中

此时的控制台:

此时宏任务队列: 队列入口 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> 队列出口

  1. 遇到p1,p1是微任务,遵循原则:在执行过程中遇到微任务就放到微任务队列中

此时的控制台:

此时宏任务队列: 队列入口 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p1 --> 队列出口

  1. 遇到console.log("同步代码Start"),是一段同步代码,遵循原则:从上至下执行所有的同步代码,所以直接打印“同步代码Start”

此时的控制台:同步代码Start

此时宏任务队列: 队列入口 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p1 --> 队列出口

  1. 遇到p2,p2是微任务,遵循原则:在执行过程中遇到微任务就放到微任务队列中,此时微任务队列中有p1,所以遵循原则:所有放到队列中的任务都采用“先进先出”的原则

此时的控制台:同步代码Start

此时宏任务队列: 队列入口 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p2 --> p1 --> 队列出口

  1. 遇到s2,s2是宏任务,遵循原则:在执行过程中遇到宏任务就放到宏任务队列中,此时宏任务队列中有s1,所以遵循原则:所有放到队列中的任务都采用“先进先出”的原则

此时的控制台:同步代码Start

此时宏任务队列: 队列入口 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p2 --> p1 --> 队列出口

  1. 遇到console.log("同步代码End"),是一段同步代码,遵循原则:从上至下执行所有的同步代码,所以直接打印“同步代码End”

此时的控制台:同步代码Start、同步代码End

此时宏任务队列: 队列入口 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p2 --> p1 --> 队列出口

  1. 此时同步代码执行完成,遵循原则:当所有同步代码执行完毕之后, 就执行微任务队列中满足需求所有回调,开始执行微任务,p1,p2都满足执行条件,遵循先进先出原则,p1先进入队列,先执行p1,p1中只有同步代码,所以直接打印“p1”

此时的控制台:同步代码Start、同步代码End、p1

此时宏任务队列: 队列入口 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p2 --> p1 --> 队列出口

  1. 此时微任务p1执行执行完毕,遵循先进先出原则,将p1移出微任务队列

此时的控制台:同步代码Start、同步代码End、p1

此时宏任务队列: 队列入口 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p2 --> 队列出口

  1. 此时微任务队列中还有未执行完毕的任务,所以执行微任务p2,p2中有同步代码console.log(p2),宏任务s3,微任务p3,遵循原则从上至下执行所有的同步代码,所以直接打印“p2”

此时的控制台:同步代码Start、同步代码End、p1、p2

此时宏任务队列: 队列入口 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p2 --> 队列出口

  1. 微任务p2接着执行,遇到宏任务s3,遵循原则:在执行过程中遇到宏任务就放到宏任务队列中,将s3放进宏任务队列中

此时的控制台:同步代码Start、同步代码End、p1、p2

此时宏任务队列: 队列入口 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p2 --> 队列出口

  1. 微任务p2接着执行,遇到微任务p3,遵循原则在执行过程中遇到微任务就放到微任务队列中,将p3放进微任务队列中

此时的控制台:同步代码Start、同步代码End、p1、p2

此时宏任务队列: 队列入口 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p3 --> p2 --> 队列出口

  1. 此时微任务p2执行完毕,遵循先进先出原则,将p2移出微任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2

此时宏任务队列: 队列入口 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p3 --> 队列出口

  1. 此时微任务队列中还有未执行完毕的任务,所以执行微任务p3,p3中只有同步代码console.log(p3),遵循原则从上至下执行所有的同步代码,所以直接打印“p3”

此时的控制台:同步代码Start、同步代码End、p1、p2、p3

此时宏任务队列: 队列入口 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p3 --> 队列出口

  1. 此时微任务p3执行执行完毕,遵循先进先出原则,将p3移出微任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3

此时宏任务队列: 队列入口 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> 队列出口

  1. 此时微任务队列为空,遵循原则当微任务队列所有满足需求回调执行完毕之后, 就执行宏任务队列中满足需求所有回调,开始执行宏任务队列

  2. 宏任务队列的所有任务都满足执行条件,遵循先进先出原则,宏任务s1先进入队列,先执行s1

  3. 执行宏任务s1,遇到console.log("s1开始"),遵循原则从上至下执行所有的同步代码,所以直接打印“s1开始”

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始

此时宏任务队列: 队列入口 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> 队列出口

  1. 接着执行宏任务s1,遇到微任务p4,遵循原则在执行过程中遇到微任务就放到微任务队列中,将p4放进微任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始

此时宏任务队列: 队列入口 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p4 --> 队列出口

  1. 接着执行宏任务s1,遇到宏任务s4,遵循原则在执行过程中遇到宏任务就放到宏任务队列中,将s4放进宏任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p4 --> 队列出口

  1. 接着执行宏任务s1,遇到微任务p5,遵循原则在执行过程中遇到微任务就放到微任务队列中,将p5放进微任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p5 --> p4 --> 队列出口

  1. 接着执行宏任务s1,遇到console.log("s1结束"),遵循原则从上至下执行所有的同步代码,所以直接打印“s1结束”

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> s1 --> 队列出口

此时微任务队列: 队列入口 --> p5 --> p4 --> 队列出口

  1. 此时宏任务s1执行完毕,遵循先进先出原则,将s1移出宏任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> 队列出口

此时微任务队列: 队列入口 --> p5 --> p4 --> 队列出口

  1. 宏任务s1执行完毕,遵循原则每执行完一个宏任务都会立刻检查微任务队列有没有被清空, 如果没有就立刻清空,开始执行微任务队列

  2. 遵循先进先出原则,先进入微任务队列的是p4,开始执行p4,p4中只有同步代码,所以直接打印“p4”

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> 队列出口

此时微任务队列: 队列入口 --> p5 --> p4 --> 队列出口

26.此时微任务p4执行完毕,遵循先进先出原则,将p4移出微任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> 队列出口

此时微任务队列: 队列入口 --> p5 -->队列出口

  1. 此时微任务队列中还有未执行完毕的任务,所以执行微任务p5,p5中只有同步代码,所以直接打印“p5”

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> 队列出口

此时微任务队列: 队列入口 --> p5 -->队列出口

28.此时微任务p5执行完毕,遵循先进先出原则,将p5移出微任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> 队列出口

此时微任务队列: 队列入口 --> 队列出口

  1. 此时微任务队列为空,遵循原则当微任务队列所有满足需求回调执行完毕之后, 就执行宏任务队列中满足需求所有回调,开始执行宏任务队列

  2. 宏任务队列的所有任务都满足执行条件,遵循先进先出原则,宏任务s2先进入队列,先执行s2

  3. 开始执行宏任务s2,遇到console.log("s2"),遵循原则从上至下执行所有的同步代码,所以直接打印“s2”

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5、s2

此时宏任务队列: 队列入口 --> s4 --> s3 --> s2 --> 队列出口

此时微任务队列: 队列入口 --> 队列出口

  1. 接着执行宏任务s2,遇到宏任务s5,遵循原则在执行过程中遇到宏任务就放到宏任务队列中,将s5放进宏任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5、s2

此时宏任务队列: 队列入口 --> s5 --> s4 --> s3 --> s2 --> 队列出口

此时微任务队列: 队列入口 --> 队列出口

  1. 接着执行宏任务s2,遇到微任务p6,遵循原则在执行过程中遇到微任务就放到微任务队列中,将p6放进微任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5、s2

此时宏任务队列: 队列入口 --> s5 --> s4 --> s3 --> s2 --> 队列出口

此时微任务队列: 队列入口 --> p6 --> 队列出口

  1. 此时宏任务s2执行完毕,遵循先进先出原则,将s2移出宏任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5、s2

此时宏任务队列: 队列入口 --> s5 --> s4 --> s3 -->队列出口

此时微任务队列: 队列入口 --> p6 --> 队列出口

  1. 宏任务s2执行完毕,遵循原则每执行完一个宏任务都会立刻检查微任务队列有没有被清空, 如果没有就立刻清空,开始执行微任务队列,微任务队列只有p6,开始执行微任务p6

  2. 开始执行微任务p6,p6中只有同步代码console.log("p6"),直接打印“p6”

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5、s2、p6

此时宏任务队列: 队列入口 --> s5 --> s4 --> s3 -->队列出口

此时微任务队列: 队列入口 --> p6 --> 队列出口

37.此时微任务p6执行完毕,遵循先进先出原则,将p6移出微任务队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5、s2、p6

此时宏任务队列: 队列入口 --> s5 --> s4 --> s3 -->队列出口

此时微任务队列: 队列入口 --> 队列出口

  1. 此时微任务队列为空,遵循原则当微任务队列所有满足需求回调执行完毕之后, 就执行宏任务队列中满足需求所有回调,开始执行宏任务队列

  2. 宏任务s3、s4、s5中只有同步代码,直接打印即可,打印完成,将任务移出队列

此时的控制台:同步代码Start、同步代码End、p1、p2、p3、s1开始、s1结束、p4、p5、s2、p6、s3、s4、s5

此时宏任务队列: 队列入口 --> 队列出口

此时微任务队列: 队列入口 --> 队列出口

  1. 至此,所有的队列都为空,事件循环结束。

三、练习题

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