本文涵蓋
- 面試題的引入
- 對事件回圈面試題執行順序的一些疑問
- 通過面試題對微任務、事件回圈、定時器等對深入理解
- 結論總結
面試題
面試題如下,大家可以先試著寫一下輸出結果,然后再看我下面的詳細講解,看看會不會有什么出入,如果把整個順序弄清楚 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.本輪回圈與次輪回圈
異步任務可以分成兩種,
- 追加在本輪回圈的異步任務
- 追加在次輪回圈的異步任務
所謂”回圈”,指的是事件回圈(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)
- timers階段次階段包括setTimeout()和setInterval()
- IO callbacks大部分的回呼事件,普通的caollback
- poll階段網路連接,資料獲取,讀取檔案等操作
- check階段setImmediate()在這里呼叫回呼
- 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() 時,這個任務會被放入到回呼佇列中,等待呼叫堆疊有空閑時事件回圈再來取走它,本輪回圈中最后執行的,
整體結論
順序的整體總結就是: 同步任務-> 本輪回圈->次輪回圈
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/159141.html
標籤:JavaScript
上一篇:Vue-cli 多頁相關配置記錄
