Javascript 的事件循環(huán)會(huì)被常常提及, 而且在實(shí)際開(kāi)發(fā)中, 經(jīng)常需要使用事件相關(guān)的知識(shí), 所以特地深入了解一下.
深入 event loop
事件循環(huán)是用來(lái)做異步任務(wù)處理的, 與之相同的做異步任務(wù)處理的還有多線程, 但是由于 javascript 的單線程特性, 最終使用 event loop 的方式.
或許你可以從下面的簡(jiǎn)略的偽代碼看出 event loop 是什么:
eventQueue = [];
event;
while(1){
if(eventQueue.length > 0) {
event = eventQueue.shift();
try {
event();
} catch(err) {
reportError(err);
}
}
}
event loop 有兩種, 一種是在瀏覽器上下文, 一種是在 worker 上下文的.瀏覽器上下文一般會(huì)至少有一個(gè) event loop, 像一個(gè) iframe, 瀏覽器窗口
都會(huì)有一個(gè) event loop, 而對(duì)于 worker 上下文的則比較簡(jiǎn)單, worker 進(jìn)程管理著一個(gè) event loop.這里著重將瀏覽器上下文的 event loop.
event loop 運(yùn)行流程
根據(jù)規(guī)范 event loop 所講的, 它的流程如下:
當(dāng)一個(gè) event loop 存在, 它會(huì)按照以下的步驟運(yùn)行:
1\. 在最老的任務(wù)隊(duì)列中取出最老的任務(wù), 如果沒(méi)有任務(wù), 那么就會(huì)跳到 microtask 隊(duì)列的執(zhí)行.這里的最老我個(gè)人理解是像任務(wù)調(diào)度算法中的等待時(shí)間最長(zhǎng)的意思.
2\. 將 event loop 當(dāng)前任務(wù)設(shè)置為最老的那個(gè)任務(wù).
3\. 執(zhí)行當(dāng)前任務(wù)隊(duì)列中最老的那個(gè)任務(wù)
4\. 將 event loop 當(dāng)前運(yùn)行任務(wù)設(shè)置為 null
5\. 將剛剛執(zhí)行的那個(gè)最老的任務(wù)從它的隊(duì)列中移除
6\. 執(zhí)行檢查 microtask 隊(duì)列算法(microtask checkpoint 這個(gè)稍后詳談), 這里先粗略理解為執(zhí)行 microtask 隊(duì)列
7\. 更新渲染(update the rendering)
8\. 如果是一個(gè) worker event loop, 但是沒(méi)有任務(wù), 并且 WorkerGlobalScope 對(duì)象的 closing flag 值為 true 的, 就銷(xiāo)毀 event loop 并中止這些步驟,
并進(jìn)行 web worker 中的 run worker 算法.
9\. 返回到第一步, 此為一個(gè)事件循環(huán).
由上面的步驟我們可以知道, 在一個(gè)循環(huán)當(dāng)中, 每執(zhí)行一個(gè)任務(wù), event loop 都會(huì)嘗試去清空 microtask 隊(duì)列, 也就是對(duì)應(yīng)的第六步.同時(shí)我們可以看到, 在做完
上面的操作之后, 才會(huì)進(jìn)行渲染操作, 防止過(guò)多的操作重復(fù)渲染造成性能問(wèn)題.
microtask checkpoint
每一個(gè) event loop 都有一個(gè) microtask 的隊(duì)列.我們從規(guī)范看到 event loop, microtask 隊(duì)列的流程如下:
如果用戶代理的 checkout point flag 值為 false 的時(shí)候, 就會(huì)按照下面的步驟進(jìn)行執(zhí)行:
1\. 設(shè)置 performing a microtask checkpoint flag 值為 true.
2\. 當(dāng) microtask 隊(duì)列不為空時(shí):
2.1 選擇隊(duì)列中最老的任務(wù)隊(duì)列
2.2 設(shè)置當(dāng)前運(yùn)行任務(wù)為選擇的最老任務(wù)
2.3 執(zhí)行這個(gè)最老的任務(wù)
2.4 設(shè)置當(dāng)前運(yùn)行任務(wù)為 null
2.5 將剛剛運(yùn)行的任務(wù)從它的任務(wù)隊(duì)列中移除.
2.6 回到 2
3\. 每一個(gè) environment settings object 他們的 responsible event loop 就是當(dāng)前 event loop, 會(huì)給 environment settings object 發(fā)出一個(gè) rejected promise 的通知.
4\. 清理 indexed db 事務(wù)
5\. 將 performing a microtask checkpoint flag 設(shè)置為 false
microtask 與 macrotask 的區(qū)別
這個(gè)應(yīng)該是 event loop 中比較核心的問(wèn)題, 究竟 timer 一類(lèi)設(shè)定的 macrotask 與 promise 一類(lèi)設(shè)定的 microtask 有什么區(qū)別?
從上面對(duì)規(guī)范的解讀可以看出, microtask 與 macrotask 在執(zhí)行上有區(qū)別, 一次 event loop 會(huì)取一個(gè) macrotask 執(zhí)行, 但是會(huì)將一個(gè) microtask 隊(duì)列
清空, 也就是說(shuō), 如果一個(gè) microtask 隊(duì)列過(guò)長(zhǎng), 確實(shí)會(huì)阻塞下一個(gè) macrotask 的開(kāi)始執(zhí)行時(shí)間.可以看出, 在異步中, js 雖然是異步非阻塞, 但是卻是使用
同步的方式來(lái)執(zhí)行 microtask 的.
另外從字面上來(lái)說(shuō), macrotask 屬于 task, 也就是大型任務(wù), microtask 屬于 job, 也就是小型任務(wù), 而對(duì)于如何更詳細(xì)的區(qū)分, 規(guī)范并沒(méi)有說(shuō), 而是從產(chǎn)生類(lèi)型
上將兩類(lèi)分開(kāi):
macroTask: setTimeout, setInterval, setImmediate, I/O, rendering
microTask: promise, process.nextTick, Object.observe, MutationObserver
我提供一個(gè)巧記的方式, 越靠近定時(shí)器一類(lèi)的就是 macrotask, 越靠近 promise 一類(lèi)的是 microtask.
至于什么時(shí)候需要使用 microtask 呢? 我覺(jué)得這個(gè)問(wèn)題很好地指出兩者(macrotask, microtask)的不同, 在你覺(jué)得需要將這個(gè)異步任務(wù)同步化的時(shí)候, 就使用
microtask , 否則就使用 macrotask.換種說(shuō)法, 也就是這個(gè)任務(wù)你需要盡可能快地執(zhí)行, 就使用 microtask.
舉個(gè)例子:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
上面的例子在 chrome 中的順序是 script start, script end, promise1, promise2, setTimeout.
event loop 的產(chǎn)生源
event loop 的 macrotask 的產(chǎn)生源有很多個(gè), 其中包括:
- DOM 元素操作, 比如以非阻塞的方式插入一個(gè)元素
- 用戶交互
- 網(wǎng)絡(luò)
- history 操作源, 比如 history.back() 等等.
task 的任務(wù)源很多, 像常見(jiàn)的 ajax, setTimeout, DOM click 事件都可以產(chǎn)生任務(wù).當(dāng)然, 不同的任務(wù)源會(huì)被加到不同的任務(wù)隊(duì)列中去.比如 ajax 操作的異步非阻塞任務(wù)就會(huì)被加到 ajax 源的
隊(duì)列中, DOM 事件產(chǎn)生的任務(wù)就會(huì)被添加到 DOM 事件的任務(wù)隊(duì)列中去.
總結(jié)
下面, 我使用一個(gè)圖來(lái)總結(jié)一下:
參考資料: