開端
在Vue
中有一個nextTick
方法,偶然一天,我發現不管程式碼的順序如何,nextTick
總是要比setTimeout
先要執行。同樣是排隊,憑什麼你nextTick
就要比我快?
開局一道題,內容全靠編。(在node下執行,答案在文末給出。)
new Promise((resolve) => {
console.log(1);
process.nextTick(() => {
console.log(2);
});
resolve();
process.nextTick(() => {
console.log(3);
});
console.log(4);
}).then(() => {
console.log(5);
});
setTimeout(() => {
console.log(6);
}, 0);
console.log(7);
複製程式碼
那麼,列印的順序到底是什麼呢?
事件迴圈
for(var i = 0; i < 5; i++){
setTimeout(function after() {
console.log(i);
}, 0);
}
複製程式碼
這道題想必大家都見得很多了,答案脫口而出5個5。為什麼呢? 答:閉包。 為什麼會產生閉包呢? 答:。。。
這一切的一切都要從女媧補天開始說起(你咋不從盤古開天開始說起呢?)。
簡單說明一下:
- 一般js是從上往下執行的,執行的時候會被放在呼叫棧中(圖中的Call Stack)。
- 然後執行到了非同步的事件(Ajax、定時器等),瀏覽器將作為Web api的一部分建立一個計時器,它將為你處理倒數計時。
- 時間到了之後就會進入到任務佇列當中(Callback Queue)。
- 事件迴圈從回撥佇列中獲取函式,並將其推到呼叫堆疊。
- 從第一步開始。
所以,即便是setTimeout(fn, 0)
(實際上最小時間間隔是4ms)也是會從下一個事件週期開始執行。
上例中,由於after函式
引用了i
並且會在下一個事件週期中被呼叫,導致了i
的記憶體沒辦法被釋放,等下個週期再來,哼 生米都煮成稀飯了。i
都被煮成5了。
關於記憶體,給大家推薦一篇我曾經翻譯的一篇文章JavaScript是如何工作的:記憶體管理 + 如何處理4個常見的記憶體洩漏。 對理解閉包也非常有幫助。
這裡我只是簡單提了一下事件迴圈,更多的細節參考文末參考文獻。
巨集任務與微任務
一個宿主環境只有一個事件迴圈,但可以有多個任務佇列。巨集任務佇列(macro task)與微任務佇列(micro task)就是其中之二。
每次事件迴圈的時候,會先執行巨集任務佇列中的任務,然後再執行微任務佇列中的任務。那麼巨集任務與微任務分別有哪些呢?
- 巨集任務:script(全域性任務), setTimeout, setInterval, setImmediate, I/O, UI rendering.
- 微任務:process.nextTick, Promise, Object.observer, MutationObserver.
new Promise((resolve) => {
resolve();
}).then(() => {
console.log(1);
});
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);
複製程式碼
按照上面的說法,應該列印出 3、2、1啊。但實際上卻列印出了3、1、2。原來像process.nextTick和Promise這種微任務,都新增的當前迴圈的微任務佇列之中。所以會比當前迴圈中的所有巨集任務要後執行,會比下個迴圈中的巨集任務要先執行。
process.nextTick 與 Promise
process.nextTick(() => {
console.log(1);
});
new Promise((resolve) => {
resolve();
}).then(() => {
console.log(2);
});
process.nextTick(() => {
console.log(3);
});
複製程式碼
為什麼我要把這兩個同屬於微任務的拎出來提一下呢?自己測試一下吧,因為結果大概會出乎你的意料。 why?
還好網際網路是強大的。沒有什麼是百度不到的,如果有,那就google。
“process.nextTick 永遠大於 promise.then,原因其實很簡單。。。在Node中,_tickCallback在每一次執行完TaskQueue中的一個任務後被呼叫,而這個_tickCallback中實質上幹了兩件事:
- nextTickQueue中所有任務執行掉(長度最大1e4,Node版本v6.9.1)
- 第一步執行完後執行_runMicrotasks函式,執行microtask中的部分(promise.then註冊的回撥)所以很明顯process.nextTick > promise.then”
小姐
Vue
中的nextTick
是巨集任務與微任務混合使用,需要手動切換。終於真相大白了。定時器:好吧 我就原諒你比我先吧。
那麼開頭題的答案是什麼呢?還是自己動手測試一下吧。
紙上得來終覺淺,覺知此事要躬行
咦,小姐?什麼小姐?你說的是
我:滾,打錯了而已。是小結。
我:什麼? 你請客!走啊走啊!
樓主被捕,完。
setImmediate
順序之爭還有一個奇怪的現象。
setImmediate(() => {
console.log(1);
});
setTimeout(() => {
console.log(2);
}, 0);
複製程式碼
然而你會發現,特麼有時候列印1、2,有時候列印2、1。你為什麼像個女人一樣啊。
nodejs官網給出的解釋是:
- setImmediate(): 是被設計用來一旦當前階段的任務執行完後執行。
- setTimeout(): 是讓程式碼延遲執行。
如果沒有在一個I/O週期執行,那麼其執行順序是不確定的。
如果在一個I/O週期執行,setImmediate
總是優先於setTimeout
執行。
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製程式碼
總是:先列印immediate再列印timeout。
參考文獻:
- 阮一峰老師的文章---JavaScript 執行機制詳解:再談Event Loop。(這篇文章有部分錯誤,建議每個例子自己嘗試,看看評論,查查資料。)
- NodeJs官方文件---The Node.js Event Loop, Timers, and process.nextTick();
- nextTick的優先順序高於promise的答案在知乎的回答中找到---Promise的佇列與setTimeout的佇列有何關聯?;
- javascript是如何工作系列中的一篇文章---How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await) 的作用;
- JavaScript 非同步、棧、事件迴圈、任務佇列