之前面試國美的時候碰到這樣的一個面試題:
console.log(1); async function fn(){ console.log(2) await console.log(3) console.log(4) //最重要的是這一步不明白
} setTimeout(()=>{ console.log(5) },0) fn(); new Promise((resolve)=>{ console.log(6) resolve(); }).then(()=>{ console.log(7) }) console.log(8)
最后的輸出結果為:1 2 3 6 8 4 7 5
一開始我以為輸出結果為:1 2 3 4 6 8 7 5 ,因為不明白 4 為什么會在這個位置輸出出來,我本來以為 4 會在 3 之后輸出出來的。
為什么 await 后面的代碼會被放到任務隊列里面?
所以上網搜,看到一篇文章,挺不錯,摘抄下來。
=============== 分割線 ===============
原文鏈接:https://segmentfault.com/a/1190000017554062
注意:原文的最后一個例子有誤,在這里我會做修改。
事件循環機制
理解js
的事件循環機制,能夠很大程度的幫我們更深層次的理解平時遇到的一些很疑惑的問題
簡單版本
下面來看一段代碼,想想它的結果和你的結果是否一樣
setTimeout(function() { console.log(1) }, 0) console.log(2) // 執行結果是 2 1
我們可以將js
的任務分為同步任務和異步任務, 按照這種分類js
的執行機制如下
- 任務執行隊列分為同步任務隊列和異步任務隊列
- 代碼執行時,遇到同步代碼,會被直接推入同步任務隊列並依次執行
- 遇到異步代碼(如
setTimeout、setInterval
), 會被直接推入異步任務隊列 - 當同步任務隊列執行完畢,這個時候異步任務隊列的任務會被依次推入同步任務隊列並依次執行
所以上面的代碼執行的時候, setTimeout()
不會被立即執行,會被推到異步任務隊列里面, 之后再執行console.log(2)
, 同步任務隊列任務執行完畢之后,會去異步任務隊列的任務會被依次推到 同步任務隊列並執行
終極版本
下面來看一段代碼,想想它的結果和你的結果是否一樣
setTimeout(function() { console.log(1) }, 0) new Promise(function(resolve, reject) { console.log(2) resolve() }).then((res) => { console.log(3) }) console.log(4) // 執行結果是 2 4 3 1
js
異步任務按照准確的划分,應該將任務分為
- 宏任務:
setTimeout
、setInterval
- 微任務: 例如
Promise.then
方法。注意new Promsie()
的時候是同步,立即執行。
注意: 現在有三個隊列: 同步隊列(也稱執行棧)、宏任務隊列、微任務隊列
所以針對這種機制,js
的事件循環機制應該是這樣的
- 遇到同步代碼,依次推入同步隊列並執行
- 當遇到
setTimeout、setInterval
,會被推到宏任務隊列 - 如果遇到
.then
,會被當作微任務,被推入微任務隊列 - 同步隊列執行完畢,然后會去微隊列取任務,直到微隊列清空。然后檢查宏隊列,去宏隊列取任務,並且每一個宏任務執行完畢都會去微隊列跑一遍,看看有沒有新的微任務,有的話再把微任務清空。這樣依次循環
console.log(1); setTimeout(() => { console.log('setTimeout'); }, 0); let promise = new Promise(resolve => { console.log(3); resolve(); }).then(data => { console.log(100); }).then(data => { console.log(200); }); console.log(2); //執行結果是:1 3 2 100 200 setTimeout
所以對於以上的代碼執行流程如下:
- 遇到同步任務先輸出1。
setTimeout
是宏任務,會先放到宏任務隊列中。new Promise
是立即執行的,所以會先輸出3。- 而
Promise.then
是微任務,會依次排列到微任務隊列中,繼續向下執行輸出2。 - 現在執行棧中的任務已經清空,再將微任務隊列清空,依次輸出100和200。
- 然后每次取出一個宏任務,因為現在只有一個宏任務,所以最后輸出
setTimeout
。
async/await (重點)
(個人注解:async/await 底層依然是 Promise,所以是微任務,只是 await 比較特殊)
async
當我們在函數前使用async
的時候,使得該函數返回的是一個Promise
對象
async function test() { return 1 // async的函數會在這里幫我們隱士使用Promise.resolve(1)
} // 等價於下面的代碼
function test() { return new Promise(function(resolve, reject) { resolve(1) }) }
可見async
只是一個語法糖,只是幫助我們返回一個Promise
而已
await
await
表示等待,是右側「表達式」的結果,這個表達式的計算結果可以是 Promise 對象的值或者一個函數的值(換句話說,就是沒有特殊限定)。並且只能在帶有async
的內部使用
使用await
時,會從右往左執行,當遇到await
時, ★★★★★會阻塞函數內部處於它后面的代碼,去執行該函數外部的同步代碼,當外部同步代碼執行完畢,再回到該函數內部執行剩余的代碼★★★★★, 並且當await
執行完畢之后,會先處理微任務隊列的代碼
下面來看一個栗子:
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' ) /** * 執行結果為: * script start * async1 start * async2 * promise1 * script end * async1 end * promise2 * setTimeout */
使用事件循環機制分析:
- 首先執行同步代碼,
console.log( 'script start' )
- 遇到
setTimeout
,會被推入宏任務隊列 - 執行
async1()
, 它也是同步的,只是返回值是Promise
,在內部首先執行console.log( 'async1 start' )
- 然后執行
async2()
, 然后會打印console.log( 'async2' )
- 從右到左會執行, 當遇到
await
的時候,阻塞后面的代碼,去外部執行同步代碼 - 進入
new Promise
,打印console.log( 'promise1' )
- 將
.then
放入事件循環的微任務隊列 - 繼續執行,打印
console.log( 'script end' )
- 外部同步代碼執行完畢,接着回到
async1()
內部, 繼續執行 await async2() 后面的代碼,執行 console.log( 'async1 end' ) ,所以打印出 async1 end 。(個人理解:async/await本質上也是Promise,也是屬於微任務的,所以當遇到await的時候,await后面的代碼被阻塞了,應該也是被放到微任務隊列了,當同步代碼執行完畢之后,然后去執行微任務隊列的代碼,執行微任務隊列的代碼的時候,也是按照被壓入微任務隊列的順序執行的) - 執行微任務隊列的代碼, 打印 console.log( 'promise2' )
- 進入第二次事件循環,執行宏任務隊列, 打印
console.log( 'setTimeout' )
為了證實自己的驗證(藍色加粗字體),特意修改國美的面試題。
修改方式一:
console.log(1); async function fn(){ console.log(2) new Promise((resolve)=>{ resolve(); }).then(()=>{ console.log("XXX") }) await console.log(3) console.log(4) } fn(); new Promise((resolve)=>{ console.log(6) resolve(); }).then(()=>{ console.log(7) }) console.log(8) // 執行結果為:1 2 3 6 8 XXX 4 7
分析:
前面的 1 2 3 6 8 不再解析,重點是后面的 XXX 4 7,由此可見 await console.log(3) 之后的代碼 console.log(4) 是被放入到微任務隊列了,代碼 console.log("XXX") 也是被壓入微任務隊列了,console.log("XXX") 是在 console.log(4) 之前,所以當同步任務執行完畢之后,執行微任務隊列代碼的時候,優先打印出來的是 XXX ,然后才是 4 。
修改方式二:
console.log(1); new Promise((resolve)=>{ resolve(); }).then(()=>{ console.log("XXX") }) async function fn(){ console.log(2) await console.log(3) console.log(4) new Promise((resolve)=>{ resolve(); }).then(()=>{ console.log("YYY") }) } fn(); new Promise((resolve)=>{ console.log(6) resolve(); }).then(()=>{ console.log(7) }) console.log(8) // 執行結果為:1 2 3 6 8 XXX 4 7 YYY
分析:
依然分析后面的 XXX 4 7 YYY 。 代碼console.log("XXX") 在 await console.log(3) 之前,所以 console.log("XXX") 被壓入微任務隊列的時機要比await console.log(3) 之后的代碼早。 同步隊列的代碼執行完畢之后,執行微任務隊列的代碼時,console.log("XXX") 的輸出要早於 console.log(4) 。而 console.log("YYY") 的代碼又是一個 Promise.then 的的微任務,會繼續被壓入新的微任務隊列。當本輪的微任務代碼執行完畢之后,再去執行新的微任務隊列的代碼,所以 YYY 會在最后輸出。
回到文章最初的面試題:
個人猜想:是不是在 console.log(4) 前面加上 await,4 是不是就可以在 3 之后打印出來了?
個人猜想修改一:
console.log(1); async function fn(){ console.log(2) await console.log(3) await console.log(4) } setTimeout(()=>{ console.log(5) },0) fn(); new Promise((resolve)=>{ console.log(6) resolve(); }).then(()=>{ console.log(7) }) console.log(8) // 執行結果為:1 2 3 6 8 4 7 5
可見,個人猜想的不對,代碼執行的時候,只要是碰見 await,執行完當前的 await 的代碼(即 await console.log(3))之后,在 await 之后的代碼(即 await console.log(4))都會被放到微任務隊列里面。
如果在 await console.log(4) 后面再加上 await 的其他代碼呢?
個人猜想修改二:
console.log(1); async function fn(){ console.log(2) await console.log(3) await console.log(4) await console.log("await之后的:",11) await console.log("await之后的:",22) await console.log("await之后的:",33) await console.log("await之后的:",44) } setTimeout(()=>{ console.log(5) },0) fn(); new Promise((resolve)=>{ console.log(6) resolve(); }).then(()=>{ console.log(7) }) console.log(8) /** * 執行結果為: * 1 * 2 * 3 * 6 * 8 * 4 * 7 * await之后的: 11 * await之后的: 22 * await之后的: 33 * await之后的: 44 * 5 */
由此可見,代碼執行的時候,只要碰見 await ,都會執行完當前的 await 之后,把 await 后面的代碼放到微任務隊列里面。但是定時器里面的 5 是最后打印出來的,可見當不斷碰見 await ,把 await 之后的代碼不斷的放到微任務隊列里面的時候,代碼執行順序是會把微任務隊列執行完畢,才會去執行宏任務隊列里面的代碼。