作为一个【实际问题驱动学习】的前端萌新,每次学习动力的激发都来自于某个问题:
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')
})
当然,这是一道为了考察知识而特意出的题目,其实并不算实际问题,而真正让我想要继续了解 js 事件机制的原因是最近对 Promise 的新体会:Promise 之所以无法使用 catch 捕获 setTimeout 回调中的错误,是因为 Promise 的 then/catch 是在 setTimeout 之前执行的。
同为异步事件,为何有先后之分呢?
因为它们是不同的异步事件类型:Microtasks 与 Macrotasks
众所周知
js 的 event loop 如下:
image.png
主线程会先执行同步代码,遇到异步代码则将其插入异步“任务队列”。待主线程空了,再会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
可是
关键在于“任务队列”不止一个,它分为 Microtasks queue 与 Macrotasks queue,分别存放着 Microtasks 与 Macrotasks:
Microtasks:
- process.nextTick
- promise
- Object.observe
- MutationObserver
Macrotasks:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI渲染
而且,一个 event loop (事件循环) 会包含多个 Macrotasks queue,也就是我们常说的 task queue (事件队列),但只包含一个 Microtasks queue,异步事件会根据自己的事件类型分别放入不同的 queue。所以有
1. task queue == macrotask queue != microtask queue
2. event loop = {
macrotask queue list: [macrotask queue0, macrotask queue1, ...],
microtask queue: ...,
}
理解了这些定义之后,再看执行原理:
事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的microtasks。当所有可执行的microtasks执行完毕之后。循环再次从macrotasks开始,找到其中一个任务队列执行完毕,然后再执行所有的microtasks,这样一直循环下去。
翻译过来就是,先执行 Microtasks queue 中的所有 Microtasks,再挑一个 Macrotasks queue 来执行其中所有 Macrotasks,然后继续执行 Microtasks queue 中的所有 Microtasks,再挑一个 Macrotasks queue 来执行其中所有 Macrotasks ……
这也就解释了,为什么同一个事件循环中的 Microtasks 会比 Macrotasks 先执行。
还要注意一点:
包裹在一个 script 标签中的 js 代码也是一个 Macrotasks
答案
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6
网友评论