作為一個【實際問題驅動學習】的前端萌新,每次學習動力的激發都來自于某個問題:
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 如下:
主線程會先執行同步代碼,遇到異步代碼則將其插入異步“任務隊列”。待主線程空了,再會去讀取"任務隊列",這就是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