宏任务与微任务

什么是宏任务和微任务

上面的循环只是一个宏观的表述,实际上异步任务之间也是有不同的,分为
宏任务(macro task) 与 微任务(micro task),最新的标准中,他们被称为
task与 jobs

  • 宏任务有哪些:script(整体代码), setTimeout, setInterval,
    setImmediate, I/O, UI rendering(渲染)

  • 微任务有哪些:process.nextTick, Promise, Object.observe(已废弃),
    MutationObserver(html5新特性)

执行过程

打个比方:

小云跟小腾一起去银行办理业务,首先需要取号之后进行排队

宏任务队列

假设银行这时只有一名柜员,那小云办理业务时,小腾只能等待

单线程,宏任务次序执行

小云首先被叫到号,于是小云先办理了存款业务

执行第一个宏任务

这时柜员会询问小云是否需要办理其他业务

执行完宏任务,开始检查是否有微任务

小云告诉柜员,她还想买一份理财产品,然后办一张信用卡,最后再兑换一些牛年兑换币

执行微任务,后续宏任务被推迟

小云离开柜台后,柜员开始给小腾办理业务

执行下一个宏任务

执行栈在执行的时候,会把宏任务放在一个宏任务的任务队列,把微任务放在一个微任务的任务队列,在当前执行栈为空的时候,主线程会
查看微任务队列是否有事件存在。如果微任务队列不存在,那么会去宏任务队列中
取出一个任务
加入当前执行栈;如果微任务队列存在,则会依次执行微任务队列中的所有任务,直到微任务队列为空(同样,是吧队列中的事件加到执行栈执行),然后去宏任务队列中取出最前面的一个事件加入当前执行栈…如此反复,进入循环。

注:

  • 宏任务和微任务的任务队列都可以有多个

  • 当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

  • 不同的运行环境 循环策略可能有不同,这里探讨chrome、node环境

//(1)

setTimeout(()=>{

console.log(1)

},100)

//(2)

setTimeout(()=>{

console.log(2)

},100)

//(3)

new Promise(function(resolve,reject){

//(4)

console.log(3)

resolve(4)

}).then(function(val){

//(5)

console.log(val);

})

//(6)

new Promise(function(resolve,reject){

//(7)

console.log(5)

resolve(6)

}).then(function(val){

//(8)

console.log(val);

})

//(9)

console.log(7)

//(10)

setTimeout(()=>{

console.log(8)

},50)

上面的代码在node和chrome环境的正确打印顺序是 3 5 7 4 6 8 1 2

  1. 全部代码在解析后加入执行栈

  2. 执行(1),宏任务,调用webapi
    setTimeout,这个方法会在100ms后将回调函数放入宏任务的任务队列

  3. 执行(2),同(1),但是会比(1)稍后一点

  4. 执行(3),同步执行new Promise,然后执行(4),直接打印 3
    ,然后resolve(4),然后.then(),把(5)放入微任务的任务队列

  5. 执行(6),同上,先打印 5
    ,再执行resolve(6),然后.then()里面的内容(8)加入到微任务的任务队列

  6. 执行(9),同步代码,直接打印 7

  7. 执行(10),同(1)和(2),只是时间更短,会在 50ms 后将回调
    console.log(8) 加入宏任务的任务队列

  8. 现在执行栈清空了,开始检查微任务队列,发现(5),加入到执行栈执行,是同步代码,直接打印
    4

  9. 任务队列又执行完了,又检查微任务队列,发现(8),打印 6

  10. 任务队列又执行完了,检查微任务队列,没有任务,再检查宏任务队列,此时如果超过了50ms的话,会发现
    console.log(8) 在宏任务队列中,于是执行 打印 8

  11. 依次打印 1 2

注:因为渲染也是宏任务,需要在一次执行栈执行完后才会执行渲染,所以如果执行栈中同时有几个同步的改变同一个样式的代码,在渲染时只会渲染最后一个

new Promise(resolve => {

console.log(‘Promise’)

resolve()

console.log(‘1q1’)

})

.then(function() {

console.log(‘promise1’)

})

.then(function() {

console.log(‘promise2’)

})

console.log(‘script end’)

Promise

1q1

script end

promise1

promise2

若不想让1q1在resolve输出用 return 看
—> [#20]{.ul}

宏任务和微任务的嵌套

new Promise((resolve, reject) => {

setTimeout(() => {

resolve();

console.log(‘setTimeout’);

}, 0);

console.log(‘promise1’);

}).then((res) => {

// 微任务

console.log(‘promise then’);

});

console.log(‘qianguyihao’);

打印结果:

promise1

qianguyihao

setTimeout

promise then

上方代码解释:在执行宏任务的过程中,创建了一个微任务。但是需要先把当前这个宏任务执行完,再去轮询异步任务的队列,进而执行微任务。

经典面试题

说出下列程序的输出顺序:

console.log(‘A’);

setTimeout(() => {

console.log(‘B’);

new Promise((resolve) => {

console.log(‘C’);

resolve();

}).then(() => {

console.log(‘D’);

});

});

new Promise((resolve) => {

console.log(‘E’);

}).then(() => {

console.log(‘F’)

});

setTimeout(() => {

console.log(‘G’);

new Promise((resolve) => {

console.log(‘H’);

}).then(() => {

console.log(‘I’);

});

})

首先执行同步任务,输出A和E

然后执行E后续的微任务,输出F

接着执行第一个宏任务中的同步任务,输出B、C

然后执行宏任务中的微任务,输出D

再之后执行第二个宏任务,输出G和H

最后执行微任务,输出I

所以完整的输出结果就是:

A

E

F

B

C

D

G

H

I