用一道大廠面試題帶你搞懂事件循環機制


本文涵蓋

  • 面試題的引入
  • 對事件循環面試題執行順序的一些疑問
  • 通過面試題對微任務、事件循環、定時器等對深入理解
  • 結論總結

面試題

面試題如下,大家可以先試着寫一下輸出結果,然后再看我下面的詳細講解,看看會不會有什么出入,如果把整個順序弄清楚 Node.js 的執行順序應該就沒問題了。

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('setTimeout0') },0)  setTimeout(function(){    console.log('setTimeout3') },3)  setImmediate(() => console.log('setImmediate'));process.nextTick(() => console.log('nextTick'));async1();new Promise(function(resolve){    console.log('promise1')    resolve();    console.log('promise2')}).then(function(){    console.log('promise3')})console.log('script end')

面試題正確的輸出結果

script startasync1 startasync2promise1promise2script endnextTickasync1 endpromise3setTimeout0setImmediatesetTimeout3

提出問題

在理解node.js的異步的時候有一些不懂的地方,使用node.js的開發者一定都知道它是單線程的,異步不阻塞且高並發的一門語言,但是node.js在實現異步的時候,兩個異步任務開啟了,是就是誰快就誰先完成這么簡單,還是說異步任務最后也會有一個先后執行順序?對於一個單線程的的異步語言它是怎么實現高並發的呢?

好接下來我們就帶着這兩個問題來真正的理解node.js中的異步(微任務與事件循環)。

Node 的異步語法比瀏覽器更復雜,因為它可以跟內核對話,不得不搞了一個專門的庫 libuv 做這件事。這個庫負責各種回調函數的執行時間,異步任務最后基於事件循環機制還是要回到主線程,一個個排隊執行。

我自己是一名從事了多年開發的web前端老程序員,目前辭職在做自己的web前端私人定制課程,今年年初我花了一個月整理了一份最適合2019年學習的web前端學習干貨,各種框架都有整理,送給每一位前端小伙伴,想要獲取的可以關注我並添加我的web前端交流裙:600610151,即可免費獲取。

詳細講解

1.本輪循環與次輪循環

異步任務可以分成兩種。

  1. 追加在本輪循環的異步任務
  2. 追加在次輪循環的異步任務

所謂”循環”,指的是事件循環(event loop)。這是 JavaScript 引擎處理異步任務的方式,后文會詳細解釋。這里只要理解,本輪循環一定早於次輪循環執行即可。

Node 規定,process.nextTick和Promise的回調函數,追加在本輪循環,即同步任務一旦執行完成,就開始執行它們。而setTimeout、setInterval、setImmediate的回調函數,追加在次輪循環。

2.process.nextTick()

1)process.nextTick不要因為有next就被好多小伙伴當作次輪循環。

2)Node 執行完所有同步任務,接下來就會執行 process.nextTick 的任務隊列。

3)開發過程中如果想讓異步任務盡可能快地執行,可以使用 process.nextTick 來完成。

3.微任務(microtack)

根據語言規格,Promise 對象的回調函數,會進入異步任務里面的”微任務”(microtask)隊列。

微任務隊列追加在 process.nextTick 隊列的后面,也屬於本輪循環。

根據語言規格,Promise 對象的回調函數,會進入異步任務里面的”微任務”(microtask)隊列。

微任務隊列追加在process.nextTick隊列的后面,也屬於本輪循環。所以,下面的代碼總是先輸出3,再輸出4。

process.nextTick(() => console.log(3));Promise.resolve().then(() => console.log(4));

// 輸出結果3,4

process.nextTick(() => console.log(1));Promise.resolve().then(() => console.log(2));process.nextTick(() => console.log(3));Promise.resolve().then(() => console.log(4));

// 輸出結果 1,3,2,4

注意,只有前一個隊列全部清空以后,才會執行下一個隊列。兩個隊列的概念 nextTickQueue 和微隊列 microTaskQueue,也就是說開啟異步任務也分為幾種,像 Promise 對象這種,開啟之后直接進入微隊列中,微隊列內的就是那個任務快就那個先執行完,但是針對於隊列與隊列之間不同的任務,還是會有先后順序,這個先后順序是由隊列決定的。

4.事件循環的階段(idle, prepare忽略了這個階段)

事件循環最階段最詳細的講解(官網:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#setimmediate-vs-settimeout)

  1. timers階段次階段包括setTimeout()和setInterval()
  2. IO callbacks大部分的回調事件,普通的caollback
  3. poll階段網絡連接,數據獲取,讀取文件等操作
  4. check階段setImmediate()在這里調用回調
  5. close階段 一些關閉回調,例如socket.on('close', ...)
  • 事件循環注意點

1)Node 開始執行腳本時,會先進行事件循環的初始化,但是這時事件循環還沒有開始,會先 完成下面的事情。

同步任務 發出異步請求 規划定時器生效的時間 執行process.nextTick()等等

最后,上面這些事情都干完了,事件循環就正式開始了。

2)事件循環同樣運行在單線程環境下,高並發也是依靠事件循環,每產生一個事件,就會加入到該階段對應的隊列中,此時事件循環將該隊列中的事件取出,准備執行之后的 Callback。

3)假設事件循環現在進入了某個階段,即使這期間有其他隊列中的事件就緒,也會先將當前隊列的全部回調方法執行完畢后,再進入到下一個階段。

5.事件循環中的setTimeOut與setImmediate

由於 setTimeout 在 timers 階段執行,而 setImmediate 在 check 階段執行。所以,setTimeout 會早於 setImmediate 完成。

setTimeout(() => console.log(1));setImmediate(() => console.log(2));

上面代碼應該先輸出1,再輸出2,但是實際執行的時候,結果卻是不確定,有時還會先輸出2,再輸出1。

這是因為 setTimeout 的第二個參數默認為0。但是實際上,Node 做不到0毫秒,最少也需要1毫秒,根據官方文檔,第二個參數的取值范圍在1毫秒到2147483647毫秒之間。也就是說,setTimeout(f, 0)等同於setTimeout(f, 1)。

實際執行的時候,進入事件循環以后,有可能到了1毫秒,也可能還沒到1毫秒,取決於系統當時的狀況。如果沒到1毫秒,那么 timers 階段就會跳過,進入 check 階段,先執行 setImmediate 的回調函數。

但是,下面的代碼一定是先輸出2,再輸出1。

const fs = require('fs');fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2));});

上面代碼會先進入 I/O callbacks 階段,然后是 check 階段,最后才是 timers 階段。因此,setImmediate才會早於setTimeout執行。

6.同步任務中async以及promise的一些誤解

  • 問題1:

在面試題中,在同步任務的過程中,不知道大家有沒有疑問,為什么不是執行完async2輸出后執行async1 end輸出,而是接着執行 promise1?

引用書中一句話:“ async 函數返回一個 Promise 對象,當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的異步操作完成,再接着執行函數體內后面的語句。”

簡單的說,先去執行后面的同步任務代碼,執行完成后,也就是表達式中的 Promise 解析完成后繼續執行 async 函數並返回解決結果。(其實還是本輪循環promise的問題,最后的resolve屬於異步,位於本輪循環的末尾。)

  • 問題2:

console.log('promise2')為什么也是在resolve之前執行?

解答:注:此內容來源與阮一峰老師的ES6書籍,調用resolve或者reject並不會終結promise的參數函數的執行。因為立即resolved的Promise是本輪循環的末尾執行,同時總是晚於本輪循環的同步任務。正規的寫法調用resolve或者reject以后,Promise的使命就完成了,后繼操作應該放在then方法后面。所以最好在它的前面加上return語句,這樣就不會出現意外

new Promise((resolve,reject) => {    return resolve(1);    //后面的語句不會執行    console.log(2);}
  • 問題3:

promise3和script end的執行順序是否有疑問?

解答:因為立即resolved的Promise是本輪循環的末尾執行,同時總是晚於本輪循環的同步任務。Promise 是一個立即執行函數,但是他的成功(或失敗:reject)的回調函數 resolve 卻是一個異步執行的回調。當執行到 resolve() 時,這個任務會被放入到回調隊列中,等待調用棧有空閑時事件循環再來取走它。本輪循環中最后執行的。

整體結論

用一道大廠面試題帶你搞懂事件循環機制

 

順序的整體總結就是: 同步任務-> 本輪循環->次輪循環


免責聲明!

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



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