推薦閱讀1:微任務、宏任務與Event-Loop
https://juejin.im/post/5b73d7a6518825610072b42b#heading-3
推薦閱讀2:js的事件循環機制:同步與異步任務(setTimeout,setInterval)宏任務,微任務(Promise,process.nextTick)
https://www.cnblogs.com/yaoyao-sun/p/10475689.html
總結事件輪詢機制,以及宏任務隊列與微任務隊列
這篇博文僅為個人理解,文章內提供一些更加權威的參考,如有片面及錯誤,歡迎指正
1. 事件輪詢(Event Loop)
事件輪詢(Event Loop) - 《你不懂JS:異步與性能》
【推薦】詳解JavaScript中的Event Loop(事件循環)機制
Javascript的宿主環境中共通的一個“線程”(一個“不那么微妙”的異步玩笑,不管怎樣)是,他們都有一種機制:在每次調用JS引擎時,可以隨着時間的推移執行你的程序的多個代碼塊兒,這稱為“事件輪詢(Event Loop)”。
換句話說,JS引擎對 時間 沒有天生的感覺,反而是一個任意JS代碼段的按需執行環境。是它周圍的環境在不停地安排“事件”(JS代碼的執行)。
js實現異步的具體解決方案
- 同步代碼直接執行
- 異步函數到了指定時間再放到異步隊列
- 同步執行完畢,異步隊列輪詢執行。
什么叫輪詢?
精簡版:當第一個異步函數執行完之后,再到異步隊列監視。一直不斷循環往復,所以叫事件輪詢。
詳細版:js引擎遇到一個異步事件后並不會一直等待其返回結果,而是會將這個事件掛起,繼續執行執行棧中的其他任務。當一個異步事件返回結果后,js會將這個事件加入與當前執行棧不同的另一個隊列,我們稱之為事件隊列。被放入事件隊列不會立刻執行其回調,而是等待當前執行棧中的所有任務都執行完畢, 主線程處於閑置狀態時,主線程會去查找事件隊列是否有任務。如果有,那么主線程會從中取出排在第一位的事件,並把這個事件對應的回調放入執行棧中,然后執行其中的同步代碼…,如此反復,這樣就形成了一個無限的循環。這就是這個過程被稱為“事件循環(Event Loop)”的原因。
事實上,事件輪詢與宏任務和微任務密切相關。
2. 宏任務和微任務
概念
在一個事件循環中,異步事件返回結果后會被放到一個任務隊列中。然而,根據這個異步事件的類型,這個事件實際上會被對應的宏任務隊列或者微任務隊列中去。並且在當前執行棧為空的時候,主線程會 查看微任務隊列是否有事件存在。如果不存在,那么再去宏任務隊列中取出一個事件並把對應的回到加入當前執行棧;如果存在,則會依次執行隊列中事件對應的回調,直到微任務隊列為空,然后去宏任務隊列中取出最前面的一個事件,把對應的回調加入當前執行棧…如此反復,進入循環。
我們只需記住當當前執行棧執行完畢時會立刻先處理所有微任務隊列中的事件,然后再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務之前執行。
在當前的微任務沒有執行完成時,是不會執行下一個宏任務的。
所以就有了那個經常在面試題、各種博客中的代碼片段:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
setTimeout就是作為宏任務來存在的,而Promise.then則是具有代表性的微任務,上述代碼的執行順序就是按照序號來輸出的。
所有會進入的異步都是指的事件回調中的那部分代碼
也就是說new Promise在實例化的過程中所執行的代碼都是同步進行的,而then中注冊的回調才是異步執行的。
在同步代碼執行完成后才回去檢查是否有異步任務完成,並執行對應的回調,而微任務又會在宏任務之前執行。
所以就得到了上述的輸出結論1、2、3、4。
+部分表示同步執行的代碼
+setTimeout(_ => {
- console.log(4)
+})
+new Promise(resolve => {
+ resolve()
+ console.log(1)
+}).then(_ => {
- console.log(3)
+})
+console.l
本來setTimeout已經先設置了定時器(相當於取號),然后在當前進程中又添加了一些Promise的處理(臨時添加業務)。
所以進階的,即便我們繼續在Promise中實例化Promise,其輸出依然會早於setTimeout的宏任務:如EXP2
宏任務
分類:
# | 瀏覽器 | Node |
---|---|---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
特性:
-
宏任務所處的隊列就是宏任務隊列
-
第一個宏任務隊列中只有一個任務:執行主線程上的JS代碼;如果遇到上方表格中的異步任務,會創建出一個新的宏任務隊列,存放這些異步函數執行完成后的回調函數。
-
宏任務隊列可以有多個
-
宏任務中可以創建微任務,但是在宏任務中創建的微任務不會影響當前宏任務的執行。(EXP3)
-
當一個宏任務隊列中的任務全部執行完后,會查看是否有微任務隊列,如果有就會優先執行微任務隊列中的所有任務,如果沒有就查看是否有宏任務隊列
微任務
分類:
# | 瀏覽器 | Node |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
特性:
-
微任務所處的隊列就是微任務隊列
-
在上一個宏任務隊列執行完畢后,如果有微任務隊列就會執行微任務隊列中的所有任務
-
new promise((resolve)=>{ 這里的函數在當前隊列直接執行 }).then( 這里的函數放在微任務隊列中執行 )
-
微任務隊列上創建的微任務,仍會阻礙后方將要執行的宏任務隊列 (EXP2)
-
由微任務創建的宏任務,會被丟在異步宏任務隊列中執行 (EXP4)
例題
EXP1: 在主線程上添加宏任務與微任務
執行順序:主線程 => 主線程上創建的微任務 => 主線程上創建的宏任務
console.log('-------start--------');
setTimeout(() => {
console.log('setTimeout'); // 將回調代碼放入另一個宏任務隊列
}, 0);
new Promise((resolve, reject) => {
for (let i = 0; i < 5; i++) {
console.log(i);
}
resolve()
}).then(()=>{
console.log('Promise實例成功回調執行'); // 將回調代碼放入微任務隊列
})
console.log('-------e
結果:
-------start--------
0
1
2
3
4
-------end--------
Promise實例成功回調執行
setTimeout
由EXP1,我們可以看出,當JS執行完主線程上的代碼,會去檢查在主線程上創建的微任務隊列,執行完微任務隊列之后才會執行宏任務隊列上的代碼
EXP2: 在微任務中創建微任務
執行順序:主線程 => 主線程上創建的微任務1 => 微任務1上創建的微任務2 => 主線程上創建的宏任務
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
結果:
1
2
3
before timeout
also before timeout
4
由EXP1,我們可以看出,在微任務隊列執行時創建的微任務,還是會排在主線程上創建出的宏任務之前執行
EXP3: 宏任務中創建微任務
執行順序:主線程 => 主線程上的宏任務隊列1 => 宏任務隊列1中創建的微任務
// 宏任務隊列 1
setTimeout(() => {
// 宏任務隊列 2.1
console.log('timer_1');
setTimeout(() => {
// 宏任務隊列 3
console.log('timer_3')
}, 0)
new Promise(resolve => {
resolve()
console.log('new promise')
}).then(() => {
// 微任務隊列 1
console.log('promise then')
})
}, 0)
setTimeout(() => {
// 宏任務隊列 2.2
console.log('timer_2')
}, 0)
console.log('========== Sync queue ==========')
// 執行順序:主線程(宏任務隊列 1)=> 宏任務隊列 2 => 微任務隊列 1 => 宏任務隊列 3
結果:
========== Sync queue ==========
timer_1
new promise
promise then
timer_2
timer_3
EXP4:微任務隊列中創建的宏任務
執行順序:主線程 => 主線程上創建的微任務 => 主線程上創建的宏任務 => 微任務中創建的宏任務
異步宏任務隊列只有一個,當在微任務中創建一個宏任務之后,他會被追加到異步宏任務隊列上(跟主線程創建的異步宏任務隊列是同一個隊列)
// 宏任務1
new Promise((resolve) => {
console.log('new Promise(macro task 1)');
resolve();
}).then(() => {
// 微任務1
console.log('micro task 1');
setTimeout(() => {
// 宏任務3
console.log('macro task 3');
}, 0)
})
setTimeout(() => {
// 宏任務2
console.log('macro task 2');
}, 1000)
console.log('========== Sync queue(macro task 1) ==========');
結果:
========== Sync queue(macro task 1) ==========
micro task 1
macro task 3
macro task 2
記住,如果把
setTimeout(() => { // 宏任務2 console.log('macro task 2'); }, 1000)改為立即執行setTimeout(() => { // 宏任務2 console.log('macro task 2'); }, 0)
那么它會在macro task 3之前執行,因為定時器是過多少毫秒之后才會加到事件隊列里
總結
微任務隊列優先於宏任務隊列執行,微任務隊列上創建的宏任務會被后添加到當前宏任務隊列的尾端,微任務隊列中創建的微任務會被添加到微任務隊列的尾端。只要微任務隊列中還有任務,宏任務隊列就只會等待微任務隊列執行完畢后再執行。
最后上一張幾乎涵蓋基本情況的例圖和例子
console.log('======== main task start ========');
new Promise(resolve => {
console.log('create micro task 1');
resolve();
}).then(() => {
console.log('micro task 1 callback');
setTimeout(() => {
console.log('macro task 3 callback');
}, 0);
})
console.log('create macro task 2');
setTimeout(() => {
console.log('macro task 2 callback');
new Promise(resolve => {
console.log('create micro task 3');
resolve();
}).then(() => {
console.log('micro task 3 callback');
})
console.log('create macro task 4');
setTimeout(() => {
console.log('macro task 4 callback');
}, 0);
}, 0);
new Promise(resolve => {
console.log('create micro task 2');
resolve();
}).then(() => {
console.log('micro task 2 callback');
})
console.log('======== main task end ========');
結果:
一旦遇到await 就立刻讓出線程,阻塞后面的代碼
等候之后,對於await來說分兩種情況
- 不是promise 對象
- 是promise對象
如果不是promise,await會阻塞后面的代碼,先執行async外面的同步代碼,同步代碼執行完畢后,在回到async內部,把promise的東西,作為await表達式的結果
如果它等到的是一個 promise 對象,await 也會暫停async后面的代碼,先執行async外面的同步代碼,等着 Promise 對象 fulfilled,然后把 resolve 的參數作為 await 表達式的運算結果。
如果一個 Promise 被傳遞給一個 await 操作符,await 將等待 Promise 正常處理完成並返回其處理結果。
具體參考https://www.cnblogs.com/fangdongdemao/p/10262209.html,但博客里的代碼運行結果需要重新試一下
第三題
async function async1() { console.log( 'async1 start' ) await async2() console.log( 'async1 end' ) } async function async2() { console.log( 'async2' ) } async1() console.log( 'script start' ) 執行結果
第四題
async function async1() { console.log( 'async1 start' ) await async2() console.log( 'async1 end' ) } async function async2() { console.log( 'async2' ) } console.log( 'script start' ) setTimeout( function () { console.log( 'setTimeout' ) }, 0 ) async1(); new Promise( function ( resolve ) { console.log( 'promise1' ) resolve(); } ).then( function () { console.log( 'promise2' ) } ) console.log( 'script end' )
如果一個 Promise 被傳遞給一個 await 操作符,await 將等待 Promise 正常處理完成並返回其處理結果。
仔細看此例子:,區分await后執行promise和非promise的區別,
async function t1 () { console.log(1) console.log(2) new Promise( function ( resolve ) { console.log( 'promise3' ) resolve(); } ).then( function () { console.log( 'promise4' ) } ) await new Promise( function ( resolve ) { console.log( 'b' ) resolve(); } ).then( function () { console.log( 't1p' ) } ) console.log(3) console.log(4) new Promise( function ( resolve ) { console.log( 'promise5' ) resolve(); } ).then( function () { console.log( 'promise6' ) } ) } setTimeout( function () { console.log( 'setTimeout' ) }, 0 ) async function t2() { console.log(5) console.log(6) await Promise.resolve().then(() => console.log('t2p')) console.log(7) console.log(8) } t1() new Promise( function ( resolve ) { console.log( 'promise1' ) resolve(); } ).then( function () { console.log( 'promise2' ) } ) t2() console.log('end');

記住,await之后的代碼必須等await語句執行完成后(包括微任務完成),才能執行后面的,也就是說,只有運行完await語句,才把await語句后面的全部代碼加入到微任務行列,所以,在遇到await promise時,必須等await promise函數執行完畢才能對await語句后面的全部代碼加入到微任務中,所以,
在等待await Promise.then微任務時,
1.運行其他同步代碼,
2.等到同步代碼運行完,開始運行await promise.then微任務,
3.await promise.then微任務完成后,把await語句后面的全部代碼加入到微任務行列,
4.根據微任務隊列,先進后出執行微任務
await 語句是同步的,await語句后面全部代碼才是異步的微任務,
所以: