【JS】异步之宏任务和微任务

217 阅读5分钟

参考链接:

一、异步

(一)原理

image.png

  1. js执行的优先级:同步 > 微队列 > 宏队列
  2. js中用来存储待执行回调函数的队列包含2个不同特定的列队,
    • 宏列队:用来保存待执行的宏任务(回调),比如:定时器回调/DOM事件回调/ajax回调
    • 微列队:用来保存待执行的微任务(回调),比如: promise的回调/MutationObserver的回调
  3. JS执行时会区别这2个队列
    • JS 引擎首先必须先执行所有的初始化同步任务代码
    • 每次准备取出第一个宏任务执行前,都要将所有的微任务一个一个取出来执行
  4. 关于promise
    then是同步执行的,then里面的回调是异步的,所以后面的回调不一定马上放到队列
    then里面的回调是否执行,要取决于他前面promise的执行结果,如果是resolve或reject就执行,如果还是pending就不会执行回调
    若没有resolve或reject,但所有.then执行完毕,返回undefined,也表示当前微任务结束

(二)例题

思路:找出同步,再找微任务和宏任务,再看是否存在无效的数据

1. 例题(难度☆)

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

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

console.log(5);
// 2 5 3 4 1
// 宏:[1]
// 微:[3 4]

2. 例题(难度☆☆☆)

const first = () => (new Promise((resolve, reject) => {
  console.log(3);

  const p = new Promise((resolve, reject) => {
    console.log(7);
    setTimeout(() => {
      console.log(5);
      resolve(6);
    }, 0);
    resolve(1);
  });

  resolve(2);

  p.then((arg) => {
    console.log(arg);
  });
}));

first().then((arg) => {
  console.log(arg);
});
console.log(4);

// 解析:
// 同:[3 7 4]
// 宏:[5]
// 微:[1 2]

答案: image.png

(1) QA:为什么resolve(6)是无效的?
  • promise状态只能改一次,resolve(6)之前已经resolve(1)过一次了,所以定时任务到了之后再执行resolve(6)是无效的
(2) QA:为什么先输出1再输出2?
  • p new的时候是马上执行的
  • p的状态执行resolve(1)后立马转为成功,所以p.then()的回调先进入微任务队列;
  • p运行完之后才执行到resolve(2),这个时候first().then()的回调才进入微任务队列,
  • 所以先输出1,再输出2

3. 例题(难度☆☆☆☆☆)

同步:1 7 2 3 8 4
宏任务:0
微任务: 4

setTimeout(() => {
  log('0')
}, 0)
new Promise((resolve, reject) => {
  log('1')
  resolve()
})
  .then(() => {
    log('2')
    new Promise((resolve, reject) => {
      log('3')
      resolve()
    })
      .then(() => {
        log('4')
      })
      .then(() => {
        log('5')
      })
  })
  .then(() => {
    log('6')
  })

new Promise((resolve, reject) => {
  log('7')
  resolve()
}).then(() => {
  log('8')
})

image.png

(1) QA:为什么3之后不输出4而输出8?
  • 8比4先进入队列
(2) QA:为什么4之后不输出5输出6?/ 为什么4之后微任务2执行完毕?
  • 因为log4之后会立马执行.then(then是同步执行的,但后面的回调log5不一定马上放到队列,因为log4还未执行完毕(pending状态),所以无法执行log5
  • 无法执行log5,但意味着log2的微任务结束了(所有.then执行完毕,返回undefined),log6先进入队列),所以先输入6再输出5
(3) 输出顺序从上到下进行分析
  1. 先执行完初始同步任务
  • 遇到定时器setTimeout,放入宏队列(log0)
  • 遇到第1个 new Promise (1) 立即执行,直接执行输出 log1, ——1
    • 跟着执行resolve()立即完成,所以后面的.then()中的回调log2被放入微队列(MicroTask2)
      • 因为后续任务log3、log4、log5、log6依赖于微任务log2,
      • 所以暂时不会将该部分放入微队列,
      • 需等执行到(log2)时才轮到
  • 遇到第2个 new Promise(2) 立即执行,直接执行输出log 7 , ——7
    • 跟着执行resolve()立即完成,所以后面的.then()中的回调log8被放入微队列(MicroTask8)
  1. 开始执行微任务
  • 执行微任务 MicroTask2,输出 2, ——2
  • 遇到第3个new Promise (3) 立即执行,输出3, ——3
    • 跟着执行resolve()立即完成,所以后面的.then()中的回调log4被放入微队列(MicroTask4)
    • 跟着执行log4后的.then(),
    • 因为此时 MicroTask4 刚进入微队列中,log5依赖于MicroTask4的执行结果,所以log5不会被放入微队列中(原理4-①②)
  • 往下走MicroTask2中没有resolve或reject,但所有.then执行完毕,微任务 MicroTask2 结束(原理4-③、疑点2)
    • 所以MicroTask2的.then()中的回调log6被放入微队列中(MicroTask6)
  • 执行微任务 MicroTask8 ,输出8,——8
  • 执行微任务 MicroTask4 ,输出4,——4
    • 此时微任务 MicroTask4 执行完毕,返回undefined,所以之前log5中的回调被放入微任务队列中(MicroTask5)
  • 执行微任务(log6),输出6,——6
  • 执行微任务(log5),输出5,——5
  1. 微任务执行完毕,执行宏任务,输出0,——0
  • 结果:1,7,2,3,8,4,6,5,0

(二)async 、await()

顺序:await > 微任务 > 宏任务

mp.weixin.qq.com/s/MErDXm9Tk…

  • async关键字是将一个同步函数变成一个异步函数,并将返回值变为promise
  • await可以放在任何异步的、基于promise的函数之前。在执行过程中,它会暂停代码在该行上,直到promise完成,然后返回结果值。而在暂停的同时,他正在等待执行的代码就有机会执行了。

[问题延伸]

1. 宏任务和微任务都有哪些⭐⭐⭐⭐⭐

宏任务:script、setTimeOut、setInterval、setImmediate
微任务:promise.then、process.nextTick、Object.observe、MutationObserver
注意:Promise是同步任务

2. 宏任务和微任务都是怎样执行的⭐⭐⭐⭐⭐

  • 执行宏任务script,
  • 进入script后,所有的同步任务主线程执行
  • 所有宏任务放入宏任务执行队列
  • 所有微任务放入微任务执行队列
  • 先清空微任务队列,
  • 再取一个宏任务,执行,再清空微任务队列
  • 依次循环