宏任务和微任务,你学废了么?

1,814 阅读8分钟

宏任务和微任务

[建议可先批阅 什么什么,竟然还有人没搞懂JavaScript的事件循环机制吧]

通过上文我们明白了事件循环机制和JavaScript的执行流程,我们知道有一个任务队列的容器,是一个队列的结构。

所有除了同步任务以外的代码,都会在工作线程中,然后按照他们的时间顺序依次有序地进入任务队列,事件循环机制会不断地从任务队列中读取进入放入执行栈中执行。

而任务队列中的异步任务又分为宏任务微任务

宏任务 (macro task)

宏任务就是JavaScript中最原始的异步任务,比如:setTimeoutsetInterValAJAX等等,在代码执行的时候会进入到工作线程中挂起,然后按照任务的时间节点顺序,依次进入任务队列,然后通过事件循环机制依次进入函数执行栈中执行。

微任务 (micro task)

微任务是后面提出的新的异步任务,在执行每一个宏任务之前,程序都会先检测是否有当次事件循环还没有被执行的微任务,如果有,则清空本次的微任务后再去执行下一个宏任务。

每一个宏任务内部可以注册当次任务微任务队列,在下一次宏任务执行前运行,微任务也按照进入队列的顺序执行。

举个🌰

十足知道吧?去十足买过东西吧?我去买东西,一群人也去买东西,我就是一个异步任务,一群人就是一群的异步任务。

我们在排队(任务队列),排到我了,后面的人要等我付完钱(执行完毕),但是,我在第一次付完钱后,又和收银员说我要一个叉烧包还要办十足会员卡(产生了微任务)。

然后收银员就又扣了我叉烧包的钱顺便办了会员卡(执行微任务)。

然后再下一个人进行付款(执行)。

所以,在JavaScript的运行环境中,代码执行流程如下所示:

  1. 首先同步代码按照顺序从上到下执行,运行过程中会注册本次的微任务和后续的宏任务。
  2. 当本次的同步代码执行完毕后,会检查一下当前的微任务队列是否有注册微任务,如果有,则优先执行微任务队列中注册的微任务。
  3. 如果当前注册的微任务中存在微任务和后续的宏任务,则会把宏任务和微任务注册到任务队列。
  4. 直到把下一个宏任务开始前的所有的微任务都执行完
  5. 首先执行最先进入队列的宏任务,谁先进入队列和之前的方式一样,看时间谁先到。执行宏任务的时候,注册本次代码产生的微任务和后续宏任务,在下一个宏任务开始前,把本次的微任务执行完。

总结

异步任务分类:宏任务微任务

当前代码产生的微任务,会在下一个宏任务执行前优先执行。当前同步任务->当前同步任务产生的微任务->下一个需要执行的宏任务.

下面来看下面的例子:

  function task1() {
    console.log("task1");
  }
  function task2() {
    setTimeout(() => {
      console.log("task2");
    }, 1000);
  }

  function task3() {
    let p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log("task3");
        resolve("p2");
      }, 0);
    });
    p2.then((res) => {
      console.log("第一个then:", res);
    });
  }
  
  let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("p1");
      resolve("p1");
    }, 1000);
  });
  p1.then((res) => {
    console.log("第一个then:", res);
    return res;
  }).then((res) => {
    console.log("第二个then:", res);
  });
  
  let p3 = new Promise((resolve, reject) => {
    console.log("p3");
    resolve("p3");
  }).then((res) => {
    console.log("第一个then:", res);
  });
  
  task1();
  task2();
  task3();
  // p3 task1 第一个then:p3 task3 第一个then:p2 第一个then:p1 第二个then:p1 task2

上面代码从上往下执行,p1的Promise回调函数是异步的,所以放入了宏任务的工作线程等待。

p3的Promise的回调函数是同步的,所以优先打印了p3。然后这个promise.then产生了微任务,把微任务放到了微任务的工作线程。

然后代码继续往下,分别执行了task1 task2 task3函数,按照顺序执行。

task1是同步的,所以直接打印了task1

task2是异步的,放入工作线程,代码继续执行。

task3是异步的,放入工作线程,至此,当前所有的同步任务执行完毕。

然后工作线程中会把当前代码产生的微任务依次放入微任务队列中,然后执行第一个.then的回调函数代码,继续执行第二个.then产生的微任务代码。

至此,当前代码的同步任务和产生的微任务都执行完毕。接下来看工作线程中哪个异步任务先到时间。

发现p2的Promise里的异步任务时间先到,然后打印task3

然后当前代码(p2的Promise里的异步任务内的同步代码)的同步任务执行完毕,发现产生了一个微任务,在下一个宏任务执行前优先执行当前代码产生的微任务,即打印第一个then:p2

p1的promise回调函数的延时时间是1000ms,task2内部的异步函数延时时间也是1000ms,但是p1的执行时间优先于task2内部的异步函数,因为代码从上往下执行。

然后依次打印:第一个then:p1 第二个then:p1 task2

常见的宏任务和微任务

宏任务

  • I/O
  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame(下次页面重绘前执行的操作,而重回会作为宏任务的一个步骤存在)

微任务

  • process.nextTick
  • MutationObserver
  • Promise.then(catch finally)

注:Promise中的回调函数是在同步任务中执行的,如果这个回调函数没有执行resolve或reject,那么回调函数内部不会有输出。

如下所示,同步在前,异步在后。Promise的回调函数是同步执行的,所以优先输出promise1promise2,同时Promise的状态也变更了,then函数的回调被注册到微任务事件中。

然后继续执行,输出end

然后同步代码执行完毕,观察异步代码的宏任务和微任务,在本次同步代码注册的微任务会在下一次的宏任务执行前执行。所以Promise.then的回调函数执行。所以输出promise then

接下来就要看setTimeoutrequestAnimationFrame两个宏任务。

setTimeout是在程序运⾏到setTimeout时⽴即注册⼀个宏任务,所以两个setTimeout的顺序⼀定是固定的timer1timer2会按照顺序输出。

requestAnimationFrame是请求下⼀次重绘事件,所以他的执⾏频率要参考浏览器的刷新率。

  setTimeout(function () {
    console.log("timer1");
  }, 0);
  requestAnimationFrame(function () {
    console.log("UI更新");
  });
  setTimeout(function () {
    console.log("timer2");
  }, 0);
  new Promise(function executor(resolve) {
    console.log("promise 1");
    resolve();
    console.log("promise 2");
  }).then(function () {
    console.log("promise then");
  });
  console.log("end");
  // promise1 -> promise2 -> end -> promise then -> UI更新 -> timer1 => timer2

补充——setTimeout(0)和requestAnimationFrame

setTimeout(fn(),0)

setTimeout是延时函数,一般第二个参数表示需要延时执行的时间,那如果是0,就真的是延时0毫秒么?

根据上面的学习,我们知道实际执行的时间是,等待同步任务执行完毕后开始延时。那如果是0毫秒,就真的是同步任务执行完毕后马上执行么?

看下面代码:

  let i = 0;
  let d = new Date().getTime();
  let d1 = new Date().getTime();
  function loop() {
    d1 = new Date().getTime();
    i++;
    if (d1 - d >= 1000) {
      d = d1;
      console.log(i);
      i = 0;
      console.log("经过了1秒");
    }
    setTimeout(loop, 0);
  }
  loop();

image.png

由此可见,差不多1秒能够执行200次,差不多5ms一次。

requestAnimationFrame 看下面代码:

  let i = 0;
  let d = new Date().getTime();
  let d1 = new Date().getTime();
  function loop() {
    d1 = new Date().getTime();
    i++;
    //当间隔时间超过1秒时执⾏
    if (d1 - d >= 1000) {
      d = d1;
      console.log(i);
      i = 0;
      console.log("经过了1秒");
    }
    requestAnimationFrame(loop);
  }
  loop();

image.png

由此可见,差不多1s能执行60次,平均16ms一次。当然不同游览器可能有不同的效果。

总结

代码执行前,同步在前,异步在后,异步任务可分为宏任务和微任务。

代码块的执行顺序是,同步任务执行,遇到异步任务会放入工作线程中挂起,工作线程会等待宏任务的时间结束,微任务直接进入当前的微任务队列(直接理解成微任务所需的时间永远比宏任务快)。然后继续执行同步任务直到同步任务执行完毕。

这时,事件循环机制就会启动,他会先从当前的微任务队列中取消息,放入执行栈中执行,知道微任务队列清空。

然后执行下一个宏任务,如果下一个宏任务产生了宏任务和微任务,则会将执行当前代码块产生的微任务,然后再执行下一个到期的宏任务。

所以宏任务执行顺序按照到期时间微任务则在当前同步的代码执行完毕后立即执行

其实每个执行栈的任务,都可以算是一个宏任务。

其中new Promise的回调函数是同步任务!!!