js事件循環機制(瀏覽器端Event Loop) 以及async/await的理解


之前面試國美的時候碰到這樣的一個面試題:

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異步任務按照准確的划分,應該將任務分為

  • 宏任務: setTimeoutsetInterval
  • 微任務: 例如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. 遇到同步任務先輸出1。
  2. setTimeout是宏任務,會先放到宏任務隊列中。
  3. new Promise是立即執行的,所以會先輸出3。
  4. Promise.then是微任務,會依次排列到微任務隊列中,繼續向下執行輸出2。
  5. 現在執行棧中的任務已經清空,再將微任務隊列清空,依次輸出100和200。
  6. 然后每次取出一個宏任務,因為現在只有一個宏任務,所以最后輸出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 */

使用事件循環機制分析:

  1. 首先執行同步代碼,console.log( 'script start' )
  2. 遇到setTimeout,會被推入宏任務隊列
  3. 執行async1(), 它也是同步的,只是返回值是Promise,在內部首先執行console.log( 'async1 start' )
  4. 然后執行async2(), 然后會打印console.log( 'async2' )
  5. 從右到左會執行, 當遇到await的時候,阻塞后面的代碼,去外部執行同步代碼
  6. 進入new Promise,打印console.log( 'promise1' )
  7. .then放入事件循環的微任務隊列
  8. 繼續執行,打印console.log( 'script end' )
  9. 外部同步代碼執行完畢,接着回到async1()內部, 繼續執行 await async2() 后面的代碼,執行 console.log( 'async1 end' ) ,所以打印出 async1 end(個人理解:async/await本質上也是Promise,也是屬於微任務的,所以當遇到await的時候,await后面的代碼被阻塞了,應該也是被放到微任務隊列了,當同步代碼執行完畢之后,然后去執行微任務隊列的代碼,執行微任務隊列的代碼的時候,也是按照被壓入微任務隊列的順序執行的)
  10. 執行微任務隊列的代碼, 打印 console.log( 'promise2' )
  11. 進入第二次事件循環,執行宏任務隊列, 打印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) 前面加上 await4 是不是就可以在 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 之后的代碼不斷的放到微任務隊列里面的時候,代碼執行順序是會把微任務隊列執行完畢,才會去執行宏任務隊列里面的代碼。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM