在說微任務與宏任務之前我們先說一下同步任務與異步任務的概念吧。
同步任務與異步任務
JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。
如果排隊是因爲計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閒着的,因爲IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等着結果出來,再往下執行。
JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。
於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。
具體來說,異步執行的運行機制如下。(同步執行也是如此,因爲它可以被視爲沒有異步任務的異步執行。)
-
所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
-
主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,在"任務隊列"之中放置一個事件。
-
一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
-
主線程不斷重複上面的3。
以上摘自廖雪峯的博客 JavaScript 運行機制詳解:再談Event Loop.
問題
我們先看一下下面的代碼,然後思考一下輸出的先後順序
setTimeout(() =>{
console.log('1')
});
new Promise((resolve) => {
console.log('2');
resolve();
}).then(() => {
console.log('3')
});
console.log('4');
按照同步與異步的概念來看,輸出順序應該是2、4、1、3.
但是,打開控制檯,輸入代碼,查看輸出,順序是這樣的2、4、3、1,發生了什麼?
想知道發生了什麼就繼續往下看吧。
宏任務與微任務
除了廣義的同步任務和異步任務,我們對任務有更精細的定義,分爲宏任務和微任務。
- 宏任務:包括整體代碼script,setTimeout,setInterval;
- 微任務:Promise,process.nextTick
注:Promise立即執行,then函數分發到“microtask”隊列,process.nextTick分發到“microtask”隊列
js引擎會把宏任務和微任務放置兩個“任務隊列”中,分別是“macrotask”隊列以及“microtask”隊列。在執行異步任務時,先執行宏任務,然後在執行微任務。
所以現在解釋一下上面問題中的輸出順序問題:
- 這段代碼作爲宏任務,進入主線程
- 遇到setTimeout,把它的回掉函數放置“macrotask”隊列中,然後接着執行下面的代碼
- 遇到Promise,new Promise會立即執行,於是輸出2,其then函數會被放置“microtask”隊列
- 遇到console.log(‘4’)直接就執行了
- 整體代碼script作爲宏任務已經執行結束,判斷“microtask”隊列中是否有可執行的微任務(then函數),然後執行,輸出3
- 至此,整個代碼的第一輪循環結束了,要開始下一輪循環,先去查看“macrotask”隊列,有setTimeout的回掉函數,然後執行,執行結束,輸出1。
- 結束。
所以上述問題的輸出順序知道怎麼肥死了吧?
盜一張事件循環,宏任務,微任務的關係圖,如下:
圖說明:進入整體代碼(宏任務)後,開始第一次循環。接着執行所有的微任務。然後再次從宏任務開始,找到符合執行條件的一個宏任務執行完畢,再執行所有的微任務。
複習一下上面的知識點,我們瞅一眼以下代碼:
console.log('1');
setTimeout(() => {
console.log('2');
process.nextTick(() => {
console.log('3');
})
setTimeout(() => {
console.log('10')
new Promise((resolve) => {
console.log('11');
resolve();
}).then(() => {
console.log('12')
})
})
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5')
})
})
process.nextTick(() => {
console.log('6');
})
new Promise((resolve) => {
console.log('7');
resolve();
}).then(() => {
console.log('8')
setTimeout(() => {
console.log('9')
})
})
console.log('10')
大聲說出答案吧:1、7、10、6、8、2、4、3、5、9、10、11、12
好吧,我們分析一下:
- 整段代碼作爲宏任務,進入主線程
- 遇到console.log(‘1’),立即執行,並向下執行
- 遇到setTimeout,把它的回掉函數fn1,放置“macrotask”隊列中,接着執行下面的代碼
- 遇到process.nextTick,把其回調函數fn2放置“microtask”隊列
- 遇到Promise,new Promise會立即執行,於是輸出7,其then函數fn3會被放置“microtask”隊列
- 遇到console.log(‘10’)直接就執行了
- 整體代碼script作爲宏任務已經執行結束,判斷“microtask”隊列中是否有可執行的微任務(fn2以及fn3),隊列具體先進先出的特點,所以先執行fn2,輸出6,然後執行fn3,輸出8,裏面包含setTimeout,把它的回調函數fn4放置“macrotask”隊列中。
- 至此,整個代碼的第一輪循環結束了,要開始下一輪循環。現在“macrotask”隊列中有fn1、fn4
- 先去查看“macrotask”隊列,先執行fn1。
- 執行fn1,遇到console.log(‘2’),就輸出,遇到process.nextTick,將其回調函數fn5放置“microtask”隊列,遇到setTimeout,把它的回掉函數fn6放置“macrotask”隊列中,遇到Promise,new Promise會立即執行,於是輸出4,其then函數fn7會被放置“microtask”隊列,即這個宏任務執行完成。“macrotask”隊列裏面有fn4、fn6。
- 現在檢查“microtask”隊列,裏面有fn5、fn7,把裏面的任務全部執行完畢,
先執行fn5,輸出3,再執行fn7,輸出5 - 至此,又一輪的循環結束了
- 再檢查“macrotask”隊列,裏面有fn4、fn6,執行fn4,輸出9.
- 而現在的“microtask”隊列是空的,再檢查“macrotask”隊列,有fn6
- 執行fn6,輸出10,遇到new Promise,輸出11,並把其回調函數fn8放置“microtask”隊列,至此宏任務fn6結束,
- 檢查“microtask”隊列,並執行fn8,輸出12,至此,“macrotask”隊列以及“microtask”隊列全部空了。
- 結束。