详解macrotasks和microtasks
macrotasks(taskQueue):宏任务 task,也是我们常说的任务队列
macrotasks 的划分如下:(注意先后顺序!)
(1)setTimeout(延迟调用)
(2)setInterval(间歇调用)
(3)setImmediate(Node 的立即调用)
(4)requestAnimationFrame(高频的 RAF)
(5)I/O(I/O 操作)
(6)UI rendering(UI 渲染)
注意:
(1)每一个 macrotask 的回调函数要放在下一车的开头去执行!
(2)只有 setImmediate 能够确保在下一轮事件循环立即得到处理
例:以下代码输出什么?
setImmediate(()=>{
console.log("aaa")
})
setImmediate(()=>{
console.log("bbb")
setImmediate(()=>{
console.log("ddd")
})
})
setImmediate(()=>{
console.log("ccc")
})
结果:aaa bbb ccc ddd
解析:
(1)setImmediate 属于 macrotasks,有 3 个外层的 setImmediate ,所以理论上有 3 班车待执行
(2)第一班车(cycle 1)
先执行第一个 setImmediate ,它的回调函数放在下一车开头(cycle 2)执行
microtask 队列为空,执行下一车
(3)第二班车(cycle 2)
开头先执行第一个 setImmediate 的回调函数,
输出:aaa
再执行第二个 setImmediate ,它的回调函数放在下下一车开头(cycle 3)执行
microtask 队列为空,执行下一车
(3)第三班车(cycle 3)
开头先执行第二个 setImmediate 的回调函数,
输出:bbb
,
注意,该 setImmediate 的回调函数也有一个 macrotask ,它的回调函数放在下下下下一车(cycle 5)的开头执行,因为 cycle 4 是外层第三个 setImmediate,所以要往后稍稍
再执行第三个 setImmediate ,它的回调函数放在下下下一车(cycle 4)执行
microtask 队列为空,执行下一车
(4)第四班车(cycle 4)
开头先执行第三个 setImmediate 的回调函数,
输出:ccc
,
microtask 队列为空,执行下一车
(5)第五班车(cycle 5)
开头执行第二个 setImmediate 内部 setImmediate 的回调函数,
输出:ddd
,
此时 microtasks 已空,
同时整段代码执行完毕。
microtasks:微任务(也称 job)调度在当前脚本执行结束后,立即执行的任务,以避免付出额外一个 task 的费用。
例如响应事件、异步操作
microtasks 的划分如下:(注意先后顺序!)
(1)process.nextTick(Node 中 定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行)
(2)Promises(详情看这篇文章:https://www.jianshu.com/p/06d16ce41ed2)
(3)Object.observe(原生观察者实现,已废弃)
(4)MutationObserver(监听 DOM change)
只有在 nextTick 空了才处理其它 microtask。(Next tick queue has even higher priority over the Other Micro tasks queue.)
例:
setImmediate(() => {
console.log('immediate');
});
Promise.resolve(1).then(x => {
console.log(x);
return x + 1;
}).then(x => {
console.log(x);
return x + 1;
}).then(x => console.log(x));
结果:1 2 3 immediate
解析:
(1)第一车(cycle 1)
setImmediate 是 macrotasks 的第一个 task,它的回调函数放在 下一车(cycle 2)的开头执行
Promise.resolve 的三个 then() 是 microtask ,直接执行,直至结束
输出:1 2 3
(2)第二车(cycle 2)
先执行 setImmediate 的回调函数,
输出:immediate
一个事件循环(eventLoop)的执行顺序(非常重要):
① 开始执行脚本。
② 取 macrotasks(taskQueue)中的第一个 task 执行,该 task 的回调函数 放在下一个 task 开头 执行。
③ 取 microtasks 中的全部 microtask 依次执行,当这些 microtask 执行结束后,可继续添加 microtask 继续执行,直到 microtask 队列为空。
④ 取 macrotasks(taskQueue)中的第二个 task 执行,该 task 的回调函数 放在下一个 task 开头 执行。
⑤ 再取 microtasks 中的全部 microtask 依次执行,当这些 microtask 执行结束后,可继续添加 microtask 继续执行,直到 microtask 队列为空。
⑥ 循环 ② ③ 直到 macrotasks、microtasks 为空。
以下代码的运行结果是什么?(重点看例3)
例1:
console.log('a');
setTimeout(() => {
console.log('b');
}, 0);
console.log('c');
Promise.resolve()
.then(() => {
console.log('d');
})
.then(() => {
console.log('e');
});
console.log('f');
结果:acfdeb
例2:
setTimeout(function(){
console.log(1)
},0);
new Promise(function(resolve){
console.log(2)
for( var i=100000 ; i>0 ; i-- ){
i==1 && resolve()
}
console.log(3)
})
.then(function(){
console.log(4)
});
console.log(5);
结果:23541
注意:Promise 的 then() 是 microtask ,所以先输出 5,再执行 microtask 队列。
例3:
console.log('start')
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve()
.then(() => {
console.log('promise 1')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve()
.then(() => {
console.log('promise 3')
})
.then(() => {
console.log('promise 4')
})
.then(() => {
clearInterval(interval)
})
}, 0)
})
}, 0)
Promise.resolve()
.then(() => {
console.log('promise 5')
})
.then(() => {
console.log('promise 6')
})
结果:
start
promise 5
promise 6
setInterval
setTimeout 1
promise 1
promise 2
setInterval
setTimeout 2
promise 3
promise 4
解析:
(1)先按照 macrotask
和 microtask
划分代码:
console.log('start')
setInterval 是 macrotask
,其回调函数在 microtask 后执行
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout 是 macrotask
,其回调函数放在下一车(cycle 2)执行
setTimeout(() => ... , 0)
Promise.resolve() 的两个 then() 是 microtask
Promise.resolve()
//microtask
.then(() => {
console.log('promise 5')
})
//microtask
.then(() => {
console.log('promise 6')
})
(2)第一班车(cycle 1
):
进栈
console.log('start')
第一个macrotask 是 setInterval,回调函数放下一车(cycle 2)的开头执行,
第二个macrotask 是 setTimeout,回调函数放下下一车(cycle 3)的开头执行,
清空栈,
输出:start
执行 microtasks,直至清空该队列,即 Promise.resolve() 的两个 then(),
输出:promise 5
promise 6
(3)第二班车(cycle 2
):
执行 setInterval 的回调,
输出:setInterval
,
同时下一个 setInterval 也是 macrotask 但要放到 下下下一车(cycle 4)执行回调,即 下下一车(cycle 3)setTimeout 的后面
此时 setInterval 中没有 microtasks,所以该队列是空的,故进行下一车(cycle 3)
(4)第三班车(cycle 3
)
执行 setTimeout 的回调,
输出 setTimeout 1
执行 microtasks,直至清空该队列,即 Promise.resolve() 的第一个和第二个 then(),
输出:promise 1
promise 2
而 第三个 then() 中的 setTimeout 是 macrotask ,放到下下下下一车(cycle 5)执行回调,
第四个 then() 是紧跟着第三个 then() 的,所以在 下下下下一车(cycle 5)执行
此时 microtasks 已空,故进行下一车(cycle 4)
(5)第四班车(cycle 4
)
由(3)得,执行 setInterval ,
输出:setInterval
此时 setInterval 中没有 microtasks,所以该队列是空的,故进行下一车(cycle 5)
同时下一个 setInterval 也是 macrotask 但要放到 下下下下下一车(cycle 6)执行回调,
(6)第五班车('cycle 5')
由(4)得,执行 setTimeout
输出:setTimeout 2
执行 microtasks,直至清空该队列,即 Promise.resolve() 的第一个和第二个 then(),
输出:promise 3
promise 4
接着执行第三个 then() --> clearInterval(interval)
,将下下下下下一车(cycle 6)要执行回调的 setInterval 清除
此时 microtasks 已空,
同时整段代码执行完毕。
参考整理(看晕了):
Node.js 事件循环一: 浅析
https://github.com/ccforward/cc/issues/47
通过microtasks和macrotasks看JavaScript异步任务执行顺序https://www.jianshu.com/p/d3ee32538b53
macrotask与microtask(看10遍不过分)
http://www.ayqy.net/blog/javascript-macrotask-vs-microtask/#articleHeader1
(完)