Node.js 調(diào)用棧

Node.js 是異步非阻塞I/O的。如何解釋 Node.js 即是單線程又是異步且非阻塞I/O的,需要理解 Node.js 的調(diào)用棧。
在寫代碼的過程中,你需要注意要避免寫同步的會(huì)阻塞線程的代碼,例如同步的網(wǎng)絡(luò)請(qǐng)求或者無限循環(huán)。
通常,每個(gè)瀏覽器的一個(gè)標(biāo)簽頁(browser tab)有一個(gè)事件輪詢(event loop),這樣可以保持每個(gè)線程獨(dú)立,避免一個(gè)網(wǎng)頁陷入無限循環(huán)或者有很多進(jìn)程的時(shí)候影響整個(gè)瀏覽器。瀏覽器環(huán)境保持多份執(zhí)行中的事件輪詢,Web Workers 同樣有一份獨(dú)立的事件輪詢。而開發(fā)者只需要關(guān)注如何在單線程中寫代碼,并且避免阻塞線程。
JS線程一旦被阻塞,UI線程就會(huì)不響應(yīng),例如用戶點(diǎn)擊頁面無反應(yīng),滾動(dòng)頁面無反應(yīng);
I/O在Node.js中是非阻塞的,例如網(wǎng)絡(luò)請(qǐng)求、文件系統(tǒng)操作。所以這也是為什么 JavaScript 存在大量的回調(diào)函數(shù)(callback)和promiseasync/await的原因。
調(diào)用棧遵循 LIFO 原則,即 Last In, First Out(后進(jìn)先出)。
事件循環(huán)(event loop)連續(xù)不斷地查看調(diào)用棧(call stack)是否有需要執(zhí)行的函數(shù),在執(zhí)行此操作時(shí),它會(huì)將找到的任何函數(shù)調(diào)用添加到調(diào)用堆棧中,并按順尋執(zhí)行每個(gè)函數(shù)調(diào)用。
一個(gè)簡(jiǎn)單的事件循環(huán)解釋:

const baz = () => console.log('baz');
const foo = () => {
  console.log('foo');
  bar();
  baz();
}
foo();

當(dāng)執(zhí)行上述代碼,首先被調(diào)用的是 foo();在foo()中,我們?cè)僬{(diào)用bar(),然后我們調(diào)用baz();此時(shí)調(diào)用棧如下:

call-stack-first-example.png

每次迭代中的事件循環(huán)都會(huì)查看調(diào)用堆棧中是否有內(nèi)容,并執(zhí)行它;

execution-order-first-example.png

直至調(diào)用棧為空。
上面的例子看起來很平常,JS查找需要執(zhí)行的函數(shù),并且按順序執(zhí)行他們。接下來我們看一下如何在棧清空的時(shí)候定義一個(gè)函數(shù)。
使用setTimeout(() => {}, 0)定義一個(gè)函數(shù),但是需要在其他函數(shù)都執(zhí)行結(jié)束之后進(jìn)行執(zhí)行。

  const bar = () => console.log('bar');
  const baz = () => console.log('baz');

  const foo = () => {
     console.log('foo');
     setTimeout(bar, 0);
     baz();
  }
  foo();

上述代碼執(zhí)行后輸出:

foo
baz 
bar

此時(shí)調(diào)用??雌饋硐襁@樣:

call-stack-second-example.png

在程序中的執(zhí)行順序如下:
execution-order-second-example.png

當(dāng) setTimeout() 被調(diào)用的時(shí)候,瀏覽器和 Node.js 開始計(jì)時(shí)。一旦時(shí)間到期(在這個(gè)例子中會(huì)立即執(zhí)行,因?yàn)槲覀冊(cè)O(shè)置時(shí)間為0),這個(gè)回調(diào)函數(shù)會(huì)被放入消息隊(duì)列(Message Queue)。

消息隊(duì)列也是用戶啟動(dòng)的事件(如單擊或鍵盤事件或獲取網(wǎng)絡(luò)數(shù)據(jù))在代碼有機(jī)會(huì)對(duì)其做出反應(yīng)之前排隊(duì)的地方。也可以是諸如onload之類的DOM事件。
事件循環(huán)優(yōu)先執(zhí)行調(diào)用棧中的任務(wù),如果沒有任務(wù)可執(zhí)行,然后去消息隊(duì)列中查找任務(wù)。

ES6任務(wù)隊(duì)列
ECMAScript 2015 引入任務(wù)隊(duì)列的概念,任務(wù)隊(duì)列使用Promises。這是一種盡可能快的執(zhí)行異步函數(shù)的方法,而不是把函數(shù)放在消息隊(duì)列里面等待執(zhí)行。

類似于游樂園的過山車:消息隊(duì)列(message queue)將你放在隊(duì)列的后面,在所有其他人的后面,你將不得不等待輪到你,而任務(wù)隊(duì)列(job queue)是快速通行證,讓你在完成前一次后立即乘坐另一次。
示例:

const bar = () => console.log('bar');
const baz = () => console.log('baz');
const foo = () => {
  console.log('foo');
  setTimeout(bar, 0);
  new Promise((resolve, reject) => 
      resolve('should  be right after baz, before bar')
  ).then(resolve => console.log(resolve))
  baz();
}
foo();

Promises(或者 async/awaitasync/await 本質(zhì)也是 Promises 實(shí)現(xiàn)的)和setTimeout()以及其他由平臺(tái)API提供的實(shí)現(xiàn)異步的函數(shù)之間有很大差異。
上述代碼的調(diào)用??雌饋硐襁@樣:

call-stack-third-example.png

理解process.nextTick()
當(dāng)我們將一個(gè)回調(diào)函數(shù)傳遞給 process.nextTick() ,這意味著我們告訴JS引擎,在當(dāng)前事件循環(huán)(event loop)結(jié)束時(shí),并在下個(gè)事件循環(huán)開始前執(zhí)行這個(gè)回調(diào)函數(shù)。

當(dāng)一個(gè)事件循環(huán)結(jié)束,JS引擎會(huì)執(zhí)行所有傳遞給 nextTick 的回調(diào)函數(shù)。通過這種方式,我們可以告訴JS引擎異步執(zhí)行任務(wù),但越快越好。

setImmediate()setTimeout(() => {}, 0) 、process.nextTick() 的區(qū)別:

  • 傳遞給 process.nextTick()的回調(diào)函數(shù),將在當(dāng)前 event loop 的操作結(jié)束后,在當(dāng)前迭代中完成。這意味著,process.nextTick()總是在setTimeoutsetImmediate之前執(zhí)行;
  • setTimeout(() => {}, 0)setImmediate(() =>{})非常相似。它們的執(zhí)行順序依賴于多種因素,但是它們都是在下個(gè)事件循環(huán)(event loop)中執(zhí)行。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容