JS 時間循環 - 宏任務與微任務
1、關於 JavaScript
JavaScript 是一門單線程語言,在最新的 html5 中提出了 Web-Worker,但 javascript 是單線程這一核心仍未改變。所以一切 javascript 版的 "多線程" 都是用單線程模擬出來的,一切 javascript 多線程都是紙老虎!
2、javascript 事件循環
既然 js 是單線程,那就像只有一個窗口的銀行,客戶需要排隊一個一個辦理業務,同理 js 任務也要一個一個順序執行。如果一個任務耗時過長,那麼後一個任務也必須等着。
那麼問題來了,假如我們想瀏覽新聞,但是新聞包含的超清圖片加載很慢,難道我們的網頁要一直卡着直到圖片完全顯示出來?因此聰明的程序員將任務分爲兩類:
-
同步任務
-
異步任務
當我們打開網站時,網頁的渲染過程就是一大堆同步任務,比如頁面骨架和頁面元素的渲染。而像加載圖片音樂之類佔用資源大耗時久的任務,就是異步任務。
關於這部分有嚴格的文字定義,但本文的目的是用最小的學習成本徹底弄懂執行機制,所以我們用導圖來說明:
導圖要表達的內容用文字來表述的話:
-
同步和異步任務分別進入不同的執行 "場所",同步的進入主線程,異步的進入 Event Table 並註冊函數。
-
當指定的事情完成時,Event Table 會將這個函數移入 Event Queue。
-
主線程內的任務執行完畢爲空,會去 Event Queue 讀取對應的函數,進入主線程執行。
-
上述過程會不斷重複,也就是常說的 Event Loop(事件循環)。
我們不禁要問了,那怎麼知道主線程執行棧爲空啊?js 引擎存在 monitoring process 進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去 Event Queue 那裏檢查是否有等待被調用的函數。
3、Promise 與 process.nextTick(callback)
Promise 的定義和功能本文不再贅述,不瞭解的讀者可以學習一下阮一峯老師的 Promise。而 process.nextTick(callback) 類似 node.js 版的 "setTimeout",在事件循環的下一次循環中調用 callback 回調函數。
我們進入正題,除了廣義的同步任務和異步任務,我們對任務有更精細的定義:
-
macro-task(宏任務):包括整體代碼 script,setTimeout,setInterval
-
micro-task(微任務):Promise,process.nextTick
不同類型的任務會進入對應的 Event Queue,比如 setTimeout 和 setInterval 會進入相同的 Event Queue。
事件循環的順序,決定 js 代碼的執行順序。進入整體代碼 (宏任務) 後,開始第一次循環。接着執行所有的微任務。然後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行所有的微任務。
事件循環的進程模型
-
選擇當前要執行的任務隊列,選擇任務隊列中最先進入的任務,如果任務隊列爲空即 null,則執行跳轉到微任務(MicroTask)的執行步驟。
-
將事件循環中的任務設置爲已選擇任務。
-
執行任務。
-
將事件循環中當前運行任務設置爲 null。
-
將已經運行完成的任務從任務隊列中刪除。
-
microtasks 步驟:進入 microtask 檢查點。
-
更新界面渲染。
-
返回第一步。
執行棧在執行完同步任務後,查看執行棧是否爲空,如果執行棧爲空,就會去檢查微任務 (microTask) 隊列是否爲空,如果爲空的話,就執行 Task(宏任務),否則就一次性執行完所有微任務。
每次單個宏任務執行完畢後,檢查微任務 (microTask) 隊列是否爲空,如果不爲空的話,會按照先入先出的規則全部執行完微任務 (microTask) 後,設置微任務 (microTask) 隊列爲 null,然後再執行宏任務,如此循環。
例子:
輸出結果:
1 7 6 8 2 4 3 5 9 11 10 12
4.process.nextTick 和 Promise 都是 Microtasks(微任務),爲什麼 process.nextTick 會先執行?
rocess.nextTick 永遠大於 promise.then,原因其實很簡單。。。在 Node 中,_tickCallback 在每一次執行完 TaskQueue 中的一個任務後被調用,而這個_tickCallback 中實質上幹了兩件事:
1、nextTickQueue 中所有任務執行掉 (長度最大 1e4,Node 版本 v6.9.1)
2、第一步執行完後執行_runMicrotasks 函數,執行 microtask(微任務)中的部分 (promise.then 註冊的回調)
所以很明顯 process.nextTick > promise.then
5、總結
(1)js 的異步
我們從最開頭就說 javascript 是一門單線程語言,不管是什麼新框架新語法糖實現的所謂異步,其實都是用同步的方法去模擬的,牢牢把握住單線程這點非常重要。
(2) 事件循環 Event Loop
事件循環是 js 實現異步的一種方法,也是 js 的執行機制。
(3)javascript 的執行和運行
執行和運行有很大的區別,javascript 在不同的環境下,比如 node,瀏覽器,Ringo 等等,執行方式是不同的。而運行大多指 javascript 解析引擎,是統一的。
(4)setImmediate
微任務和宏任務還有很多種類,比如 setImmediate 等等,執行都是有共同點的,有興趣的同學可以自行了解。
(5) 最後的最後
-
javascript 是一門單線程語言
-
Event Loop 是 javascript 的執行機制
**6、深入淺出分析 process.nextTick() **
process.nextTick() 是 Node 的一個定時器,讓任務可以在指定的時間運行。其中 Node 一共提供了 4 個定時器,它們分別是 setTimeout()、setInterval()、setImmediate()、process.nextTick()。
process.nextTick() 這個名字有點誤導,它是在本輪循環執行的,而且是所有異步任務裏面最快執行的。
Node 執行完所有同步任務,接下來就會執行 process.nextTick 的任務隊列。所以,下面這行代碼是第二個輸出結果。
process.nextTick(() => console.log(3));
基本上,如果你希望異步任務儘可能快地執行,那就使用 process.nextTick。
根據語言規格,Promise 對象的回調函數,會進入異步任務裏面的” 微任務”(microtask)隊列。
微任務隊列追加在 process.nextTick 隊列的後面,也屬於本輪循環。所以,下面的代碼總是先輸出 3,再輸出 4。
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4)); // 3 // 4
注意,只有前一個隊列全部清空以後,纔會執行下一個隊列。
process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4)); // 1 // 3 // 2 // 4
上面代碼中,全部 process.nextTick 的回調函數,執行都會早於 Promise 的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/sryIBteJ_9Dqc0bZdFTX9w