事件循环(EventLoop)

进程和线程

在了解什么是js的事件循环之前,必须要弄清楚什么叫线程,什么又叫进程?当我们的电脑刚组装好,是没有办法使用的,这个时候我们就需要安装操作系统,操作系统就像是我们和硬件之间的桥梁。

  1. 进程:进程是指计算机中已运行的程序,是分时系统的基本运作单位。这时维基百科对进程的解释,还是那句话,维基百科喜欢用概念解释概念,太抽象了。举个通俗的例子,我们的电脑每打开一个软件,就会开辟一个进程,用于服务这个软件
  2. 线程:线程是指操作系统能够进行运算调度最小的单位,大部分情况下他被包含在进程中。也就是说一个进程可以拥有多个线程。

事件队列

我们都知道JavaScript是单线程语言,那么他遇到耗时的代码会如何处理呢?比如说有一个十秒的定时器,难不成站在那里等待?这明显不现实,所以这个时候就需要一个地方用于存放一些耗时的任务,我先继续往下面执行,等你该运行了我就回过头来执行其中的代码,那么问题又来了,这个队列由谁来维护?自然不可能是JavaScript,别忘记JavaScript是单线程的,如果是JavaScript来维护,那么其他代码怎么处理。这个维护队列的兄弟就是我们常见的浏览器。

​ 浏览器会帮我们把一些耗时的操作维护起来,等到可以执行的时候就帮我们放到任务队列,然后通知我们的就是js可以执行了,你会发现这就形成了一个闭环。这就是所谓的事件循环。

宏任务队列和微任务队列

  • 宏任务:正常的异步任务都是宏任务,最常见的就是定时器(setInterval, setImmediate, setTimeout)、DOM监听

  • 微任务:微任务出现比较晚,queueMicrotask、Promise和async属于微任务

​ 这个时候就有点懵逼了,两个队列,我都有待执行的代码的话,到底先执行那个?这里是有规范的,在执行任何的宏任务之前都保证微任务队列中没有待执行的任务。当然在去任务队列找任务执行之前要先保证script最顶层的代码运行完毕,比如说一些函数调用、打印…

整点例子加深记忆

  1. 第一个例子

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

    new Promise(resolve => {
    console.log("promise1");
    resolve();
    }).then(() => {
    console.log("then1");
    });
    • 当js进程遇到,定时器(宏任务),发现他并不需要等待,直接丢给宏任务队列,然后又遇到一个Promise(微任务),他不会被加入什么队列,直接执行,然后打印出promise1,然后调用resolve,then会被加入微任务队列的。

    • 记得上面说过的,微任务的执行时机优先于宏任务,所以打印then1。

    • 微任务执行完毕,回过头来看宏任务,先执行setTimeout1,然后执行new Promise,直接调用resolve(),将then方法丢到微任务队列,然后发现没有其他逻辑,宏任务队列执行完毕

    • 然后回到微任务队列,执行then方法,又遇到一个promise,执行他,然后运行到resolve方法,将then方法丢到微任务队列,然后继续往下执行,发现有个console.log(),直接执行,然后宏任务队列执行完毕

    • 然后又来到微任务队列,调用then方法,最后输出then4

    • 经过一通分析,我给出的结论是:promise1 —-> then1 ——> setTimeout1 —-> then2 —-> then4

    • 看看结果

      执行结果

    • 结论:当js执行下宏任务的时候会先去看看为任务队列有没有东西,有东西就先执行,无论他是什么时候添加进来的,只要当前宏任务执行玩了,准备在宏任务队列里面执行下一个宏任务之前都要去瞄一眼微任务队列有没有东西。

  2. 第二个例子

    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
    async function async1() {
    console.log("async1 start");
    await async2();
    console.log("async1 end");
    }

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

    console.log("script start");

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

    async1();

    new Promise(resolve => {
    console.log("promise1");
    resolve();
    }).then(() => {
    console.log("promise2");
    });
    console.log("script end");
    //执行结果:
    // script start
    // async1 start
    // async2
    // promise1
    // script end
    // async1 end
    // promise2
    // setTimeout
    • 分析代码,async1和async2都是定义函数,并不执行,所以可以直接跳过

    • 最先输出script start

    • 遇到定时器,丢进宏任务队列

    • 执行async1,默认是同步代码,所以直接打印async1 start

    • 然后调用async2,打印async2

    • 这里有个坑,async标志的函数默认返回的就不是一个undefined了,而是一个包裹undefined的promise,所以await后面的代码其实会被作为then方法调用,then方法是要被要加微任务队列的

    • 所以将async1 end丢入微任务队列,然后继续往下执行到new Promise

    • 直接输出promise1,遇到resolve(),调用then方法,所以promise2会被加入微任务队列

    • 然后继续输出script end

    • 然后执行宏任务之前先看微任务,发现有需要执行的代码,然后依次执行,也就是async1 end ——> promise2

    • 然后执行完微任务,最后宏任务,也就是那个定时器,输出setTimeout

      所以我们给出的结果是:script start —-> async start —-> async2 —-> promise1 —-> script end —-> async1 end —-> promise2 —-> setTimeout

    • 代码运行结果:

      1
      2
      3
      4
      5
      6
      7
      8
      // script start
      // async1 start
      // async2
      // promise1
      // script end
      // async1 end
      // promise2
      // setTimeout