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

開端

Vue中有一個nextTick方法,偶然一天,我發現無論代碼的順序如何,nextTick老是要比setTimeout先要執行。一樣是排隊,憑什麼你nextTick就要比我快? javascript

開局一道題,內容全靠編。(在node下運行,答案在文末給出。)html

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);
複製代碼

那麼,打印的順序究竟是什麼呢?java

事件循環

for(var i = 0; i < 5; i++){
    setTimeout(function after() {
        console.log(i);
    }, 0);
}
複製代碼

這道題想必你們都見得不少了,答案脫口而出5個5。爲何呢? 答:閉包。 爲何會產生閉包呢? 答:。。。node

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

簡單說明一下:api

  1. 通常js是從上往下執行的,執行的時候會被放在調用棧中(圖中的Call Stack)。
  2. 而後執行到了異步的事件(Ajax、定時器等),瀏覽器將做爲Web api的一部分建立一個計時器,它將爲你處理倒計時。
  3. 時間到了以後就會進入到任務隊列當中(Callback Queue)。
  4. 事件循環從回調隊列中獲取函數,並將其推到調用堆棧。
  5. 從第一步開始。

因此,即使是setTimeout(fn, 0)(實際上最小時間間隔是4ms)也是會從下一個事件週期開始執行。promise

上例中,因爲after函數引用了i而且會在下一個事件週期中被調用,致使了i的內存沒辦法被釋放,等下個週期再來,哼 生米都煮成稀飯了。i都被煮成5了。瀏覽器

關於內存,給你們推薦一篇我曾經翻譯的一篇文章JavaScript是如何工做的:內存管理 + 如何處理4個常見的內存泄漏。 對理解閉包也很是有幫助。session

這裏我只是簡單提了一下事件循環,更多的細節參考文末參考文獻。閉包

宏任務與微任務

一個宿主環境只有一個事件循環,但能夠有多個任務隊列。宏任務隊列(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);
複製代碼

按照上面的說法,應該打印出 三、二、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中實質上幹了兩件事:

  1. nextTickQueue中全部任務執行掉(長度最大1e4,Node版本v6.9.1)
  2. 第一步執行完後執行_runMicrotasks函數,執行microtask中的部分(promise.then註冊的回調)因此很明顯process.nextTick > promise.then」

小姐

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

那麼開頭題的答案是什麼呢?仍是本身動手測試一下吧。

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

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

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

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

樓主被捕,完。


setImmediate

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

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

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

然而你會發現,特麼有時候打印一、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。


參考文獻:

  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 異步、棧、事件循環、任務隊列
相關文章
相關標籤/搜索