背景
在研究js的异步的实现方式的时候,发现了JavaScript 中的 macrotask 和 microtask 的概念。在查阅了一番资料之后,对其中的执行机制有所了解,下面整理出来,希望可以帮助更多人。
先了解一下js的任务执行机制
首先,javascript是单线程的,所以只能通过异步解决性能问题(否则,如果前面一个任务阻塞了,那么后续的任务都要等待,这种效果是无法接受的)。js在执行代码时存在着两个比较重要的东西:执行栈和任务队列,这两个东西都是用来存储任务的,区别在于:执行栈里面存着的都是同步任务,也就是要按顺序执行的任务;而任务队列中存着的是一些异步任务,这些异步任务一定要等到执行栈清空后才会执行(这句话很重要)。关于任务队列,它还分成两种,一种叫作macrotask queue(姑且这么命名,因为严格来说规范中只有说task,并没有提到macrotask这个概念。这里为了容易区分,可以理解为macrotask=task!=microtask),另一种叫作microtask queue。如果同时考虑node环境和浏览器环境的话,这两种任务分别对应以下api:
microtasks:
- process.nextTick
- promise
- Object.observe
- MutationObserver
macrotasks:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI渲染
- script标签中的整体代码
javascript在执行时,先从 macrotasks 队列开始执行,取出第一个 macrotask 放入执行栈执行,在执行过程中,如果遇到 macrotask,则将该 macrotask 放入 macrotask 队列,继续运行执行栈中的后续代码。如果遇到microtask,那么将该microtask放入microtask队列,继续向下运行执行栈中的后续代码。当执行栈中的代码全部执行完成后,从microtasks队列中取出所有的microtask放入执行栈执行。执行完毕后,再从macrotasks 队列取出下一个macrotask放入执行栈。然后不断重复上述流程。这一过程也被称作事件循环(Event Loop)。
javascript就是通过这种机制来实现异步的。主线程会暂时存储I/O等异步操作,直接向下执行,当某个异步事件触发时,再通知主线程执行相应的回调函数,通过这种机制,javascript避免了单线程中异步操作耗时对后续任务的影响。
图解事件循环流程
根据图中描述,一次事件循环的执行步骤如下:
1、从macrotask queue中取出最早的任务
2、在执行栈中执行第一步取出的任务
如果任务中存在microtask,将其压入到microtask queue中
如果任务中存在macrotask,将其压入到macrotask queue中
直到执行完毕
3、执行栈设置为null
4、从macrotask queue中删除执行过的macrotask
5、取出microtask queue中的全部任务,放入执行栈,
如果任务中存在microtask,将其压入到microtask queue中
如果任务中存在macrotask,将其压入到macrotask queue中
注意:这里产生的microtask(也就是microtask产生的microtask )也会在这一步骤中执行。
直到当前microtask queue为空,此步骤结束。
6、执行第一步的操作
实例验证
我们执行如下一段代码,用上面的思路执行,看一下结果是否和预期的一致。
console.log('start')
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve()
.then(() => {
console.log('promise 3')
})
.then(() => {
console.log('promise 4')
})
.then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve()
.then(() => {
console.log('promise 5')
})
.then(() => {
console.log('promise 6')
})
.then(() => {
clearInterval(interval)
})
}, 0)
})
}, 0)
Promise.resolve()
.then(() => {
console.log('promise 1')
})
.then(() => {
console.log('promise 2')
})
按照上面的思路,我们来理一下,预测一下执行结果,看看实际效果是否是这样的。
执行流程:
第一轮:
1、首先这一整段js代码作为一个macrotask先被执行
2、遇到console.log('start'),输出start
3、遇到setInterval,回调函数作为macrotask压入到macrotask queue中,
此时macrotask queue:[setInterval]
4、遇到setTimeout,回调函数作为macrotask压入到macrotask queue中,
此时macrotask queue:[setInterval,setTimeout1]
5、遇到Promise,并且调用了resolve方法,触发了回调,回调作为microtask压入到microtask queue中
此时microtask queue:[promise 1,promise 2]
6、执行栈为空,将microtask queue中的任务放入执行栈
7、执行microtask queue中Promise的回调任务,分别打印promise 1,promise 2
8、执行栈为空,microtask queue为空,开始下一轮事件循环
目前的console中打印内容:
start
promise 1
promise 2
目前macrotask queue:[setInterval,setTimeout1]
第二轮:
1、从macrotask queue中取出最早的任务,这里对应的是第一轮中第3步的回调函数:console.log('setInterval'),输出setInterval
2、setInterval的回调函数作为macrotask压入到macrotask queue中
此时macrotask queue:[setTimeout1,setInterval]
3、执行栈为空,microtask queue为空,开始下一轮事件循环
目前的console中打印内容:
start
promise 1
promise 2
setInterval
目前macrotask queue:[setTimeout1,setInterval]
第三轮:
1、从macrotask queue中取出最早的任务,目前是setTimeout1的回调,将取出的任务放入执行栈执行
2、遇到console.log('setTimeout 1'),输出setTimeout 1
3、遇到Promise,并且调用了resolve方法,触发回调,回调作为microtask压入到microtask queue中
此时microtask queue:[promise 3,promise 4,() => {setTimeout 2}]
4、执行栈为空,将microtask queue中的任务放入执行栈
5、执行microtask queue中Promise的回调任务:
输出promise 3
输出promise 4
将setTimeout 2压入macrotask queue
6、执行栈为空,microtask queue为空,开始下一轮事件循环
目前的console中打印内容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
目前macrotask queue:[setInterval,setTimeout2]
第四轮:
1、从macrotask queue中取出最早的任务,这里对应的是setInterval,输出setInterval
2、setInterval的回调函数作为macrotask压入到macrotask queue中
此时macrotask queue:[setTimeout2,setInterval]
3、执行栈为空,microtask queue为空,开始下一轮事件循环
目前的console中打印内容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
目前macrotask queue:[setTimeout2,setInterval]
第五轮:
1、从macrotask queue中取出最早的任务,目前是setTimeout2的回调,将取出的任务放入执行栈执行
2、遇到console.log('setTimeout 2')输出setTimeout 2
3、遇到Promise,并且调用了resolve方法,触发回调,回调作为microtask压入到microtask queue中
此时microtask queue:[promise 5,promise 6,() => {clearInterval}]
4、执行栈为空,将microtask queue中的任务放入执行栈
5、执行microtask queue中Promise的回调任务:
输出promise 5
输出promise 6
clearInterval清空setInterval计时器
6、执行栈为空,microtask queue为空,macrotask queue为空,任务结束。
最终的console中打印内容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6
通过图片可以看到,结果跟我们的预期一致,在promise2的后面作为方法的返回值,多打印了一个undefined,这个应该好理解的。
这里面有个小问题,就是在不同的环境下(node/浏览器),promise4后面的setInterval表现可能会有差异,这里可能跟setTimeout和setInterval的最小间隔有关,虽然我们写成0ms,但实际上这个最小值是有限制的,现阶段不同组织和不同的js引擎实现机制存在差异,不过这个问题不在本次讨论范围之内了。如果我们将上述代码中setInterval的间隔设置为10,那么整个执行流程将严格符合我们的预期。
有什么用?
- 后续我们在代码中使用Promise,setTimeout时,思路将更加清晰,用起来更佳得心应手。
- 在阅读一些源码时,对于一些setTimeout相关的骚操作可以理解的更加深入。
- 理解javascript中的任务执行流程,加深对异步流程的理解,少犯错误。
总结
- js事件循环总是从一个macrotask开始执行
- 一个事件循环过程中,只执行一个macrotask,但是可能执行多个microtask
- 执行栈中的任务产生的microtask会在当前事件循环内执行
- 执行栈中的任务产生的macrotask要在下一次事件循环才会执行
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。