當瀏覽器或者Node拿到一段代碼時首先做的就是傳遞給JavaScript引擎,並且要求它去執行。
然而,執行 JavaScript 並非一錘子買賣,宿主環境當遇到一些事件時,會繼續把一段代碼傳遞給 JavaScript 引擎去執行,此外,我們可能還會提供 API 給 JavaScript 引擎,比如 setTimeout 這樣的 API,它會允許 JavaScript 在特定的時機執行。
所以,我們首先應該形成一個感性的認知:一個 JavaScript 引擎會常駐於內存中,它等待着我們(宿主)把 JavaScript 代碼或者函數傳遞給它執行。
由於我們這里講的是JavaScript 語言,我們把宿主發起的任務稱為宏觀任務,把 JavaScript 引擎發起的任務稱為微觀任務。
宏觀和微觀任務
JavaScript 引擎等待宿主環境分配宏觀任務,在操作系統中,通常等待的行為都是一個事件循環,所以在 Node 術語中,也會把這個部分稱為事件循環。我們用偽代碼來表示,大概就是:
while (TRUE) {
r = wait();
execute(r);
}
我們可以看到,整個循環做的事情基本上就是反復“等待 - 執行”。當然,實際的代碼中並沒有這么簡單,還有要判斷循環是否結束、宏觀任務隊列等邏輯。

有了宏觀任務和微觀任務機制,我們就可以實現 JS 引擎級和宿主級的任務了,例如:Promise 永遠在隊列尾部添加微觀任務。setTimeout 等宿主 API,則會添加宏觀任務。在執行完一個宏觀任務后再執行后一個宏觀任務。
接下來我們介紹一下 Promise。
Promise 是 JavaScript 語言提供的一種標准化的異步管理方式,它的總體思想是,需要進行 io、等待或者其它異步操作的函數,不返回真實結果,而返回一個“承諾”,函數的調用方可以在合適的時機,選擇等待這個承諾兌現(通過 Promise 的 then 方法的回調)。(建議不是很了解promise的可以去看一下阮一峰老師的ES6標准入門)
Promise 的 then 回調是一個異步的執行過程,下面我們就來研究一下 Promise 函數中的執行順序,我們來看一段代碼示例:
1 var r = new Promise(function(resolve, reject) { 2 console.log("a"); 3 resolve() 4 }); 5 r.then(() => console.log("c")); 6 console.log("b")
我們執行這段代碼后,注意輸出的順序是 a b c。在進入 console.log(“b”) 之前,毫無疑問 r 已經得到了 resolve,但是 Promise 的 resolve 始終是異步操作,所以 c 無法出現在 b 之前。
接下來我們試試跟 setTimeout 混用的 Promise。為了理解微任務始終先於宏任務,將Promise改成耗時1秒。
1 setTimeout(()=>console.log("d"), 0) 2 var r = new Promise(function(resolve, reject){ 3 resolve() 4 }); 5 r.then(() => { 6 var begin = Date.now(); 7 while(Date.now() - begin < 1000); 8 console.log("c1") 9 new Promise(function(resolve, reject){ 10 resolve() 11 }).then(() => console.log("c2")) 12 });
這里我們強制了 1 秒的執行耗時,這樣,我們可以確保任務 c2 是在 d 之后被添加到任務隊列。
我們可以看到,即使耗時一秒的 c1 執行完畢,再 enque 的 c2,仍然先於 d 執行了,這很好地解釋了微任務優先的原理
通過一系列的實驗,我們可以總結一下如何分析異步執行的順序:
1、首先我們分析有多少個宏任務;
2、在每個宏任務中,分析有多少個微任務;
3、根據調用次序,確定宏任務中的微任務執行次序;
4、根據宏任務的觸發規則和調用次序,確定宏任務的執行次序;(同一個宏任務下的微任務始終於這個宏任務前執行:可參考:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ )
5、確定整個順序。
1 function sleep(duration) { 2 return new Promise(function(resolve, reject) { 3 console.log("b"); 4 setTimeout(resolve,duration); 5 }) 6 } 7 console.log("a"); 8 sleep(5000).then(()=>console.log("c"));
這是一段非常常用的封裝方法,利用 Promise 把 setTimeout 封裝成可以用於異步的函數。
我們首先來看,setTimeout 把整個代碼分割成了 2 個宏觀任務,這里不論是 5 秒還是 0 秒,都是一樣的。
第一個宏觀任務中,包含了先后同步執行的 console.log(“a”); 和 console.log(“b”);。
setTimeout 后,第二個宏觀任務執行調用了 resolve,然后 then 中的代碼異步得到執行,所以調用了 console.log(“c”),最終輸出的順序才是: a b c。
這里應該更能了解Promise和setTimeout以及其他代碼的執行順序了。
本文參考於 winter 老師的重學前端課程,有興趣的小伙伴也可以去了解一下。
