使用Vue的nextTick引發的執行順序之爭

王興欣發表於2018-02-02

開端

Vue中有一個nextTick方法,偶然一天,我發現不管程式碼的順序如何,nextTick總是要比setTimeout先要執行。同樣是排隊,憑什麼你nextTick就要比我快?

使用Vue的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。為什麼呢? 答:閉包。 為什麼會產生閉包呢? 答:。。。

這一切的一切都要從女媧補天開始說起(你咋不從盤古開天開始說起呢?)。

使用Vue的nextTick引發的執行順序之爭

簡單說明一下:

  1. 一般js是從上往下執行的,執行的時候會被放在呼叫棧中(圖中的Call Stack)。
  2. 然後執行到了非同步的事件(Ajax、定時器等),瀏覽器將作為Web api的一部分建立一個計時器,它將為你處理倒數計時。
  3. 時間到了之後就會進入到任務佇列當中(Callback Queue)。
  4. 事件迴圈從回撥佇列中獲取函式,並將其推到呼叫堆疊。
  5. 從第一步開始。

所以,即便是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?

使用Vue的nextTick引發的執行順序之爭

還好網際網路是強大的。沒有什麼是百度不到的,如果有,那就google。

“process.nextTick 永遠大於 promise.then,原因其實很簡單。。。在Node中,_tickCallback在每一次執行完TaskQueue中的一個任務後被呼叫,而這個_tickCallback中實質上幹了兩件事:

  1. nextTickQueue中所有任務執行掉(長度最大1e4,Node版本v6.9.1)
  2. 第一步執行完後執行_runMicrotasks函式,執行microtask中的部分(promise.then註冊的回撥)所以很明顯process.nextTick > promise.then”

小姐

Vue中的nextTick是巨集任務與微任務混合使用,需要手動切換。終於真相大白了。定時器:好吧 我就原諒你比我先吧。

那麼開頭題的答案是什麼呢?還是自己動手測試一下吧。

紙上得來終覺淺,覺知此事要躬行

咦,小姐?什麼小姐?你說的是

使用Vue的nextTick引發的執行順序之爭

我:滾,打錯了而已。是小結

我:什麼? 你請客!走啊走啊!

樓主被捕,完。


setImmediate

順序之爭還有一個奇怪的現象。

setImmediate(() => {
    console.log(1);
});

setTimeout(() => {
    console.log(2);
}, 0);
複製程式碼

然而你會發現,特麼有時候列印1、2,有時候列印2、1。你為什麼像個女人一樣啊。

使用Vue的nextTick引發的執行順序之爭

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。


參考文獻:

  1. 阮一峰老師的文章---JavaScript 執行機制詳解:再談Event Loop。(這篇文章有部分錯誤,建議每個例子自己嘗試,看看評論,查查資料。)
  2. NodeJs官方文件---The Node.js Event Loop, Timers, and process.nextTick();
  3. nextTick的優先順序高於promise的答案在知乎的回答中找到---Promise的佇列與setTimeout的佇列有何關聯?;
  4. javascript是如何工作系列中的一篇文章---How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await) 的作用;
  5. JavaScript 非同步、棧、事件迴圈、任務佇列

相關文章