14. Node.js 事件循環(huán)

圖片來源網(wǎng)絡,侵刪

介紹

事件循環(huán)是了解 Node.js 最重要的方面之一。

為什么這么重要? 因為它闡明了 Node.js 如何做到異步且具有非阻塞的 I/O,所以它基本上闡明了 Node.js 的“殺手級應用”,正是這一點使它成功了。

Node.js JavaScript 代碼運行在單個線程上。 每次只處理一件事。

這個限制實際上非常有用,因為它大大簡化了編程方式,而不必擔心并發(fā)問題。

只需要注意如何編寫代碼,并避免任何可能阻塞線程的事情,例如同步的網(wǎng)絡調用或無限的循環(huán)。

通常,在大多數(shù)瀏覽器中,每個瀏覽器選項卡都有一個事件循環(huán),以使每個進程都隔離開,并避免使用無限的循環(huán)或繁重的處理來阻止整個瀏覽器的網(wǎng)頁。

該環(huán)境管理多個并發(fā)的事件循環(huán),例如處理 API 調用。 Web 工作進程也運行在自己的事件循環(huán)中。

主要需要關心代碼會在單個事件循環(huán)上運行,并且在編寫代碼時牢記這一點,以避免阻塞它。

阻塞事件循環(huán)

任何花費太長時間才能將控制權返回給事件循環(huán)的 JavaScript 代碼,都會阻塞頁面中任何 JavaScript 代碼的執(zhí)行,甚至阻塞 UI 線程,并且用戶無法單擊瀏覽、滾動頁面等。

JavaScript 中幾乎所有的 I/O 基元都是非阻塞的。 網(wǎng)絡請求、文件系統(tǒng)操作等。 被阻塞是個異常,這就是 JavaScript 如此之多基于回調(最近越來越多基于 promise 和 async/await)的原因。

調用堆棧

調用堆棧是一個 LIFO 隊列(后進先出)。

事件循環(huán)不斷地檢查調用堆棧,以查看是否需要運行任何函數(shù)。

當執(zhí)行時,它會將找到的所有函數(shù)調用添加到調用堆棧中,并按順序執(zhí)行每個函數(shù)。

你知道在調試器或瀏覽器控制臺中可能熟悉的錯誤堆棧跟蹤嗎? 瀏覽器在調用堆棧中查找函數(shù)名稱,以告知你是哪個函數(shù)發(fā)起了當前的調用:


圖片來源網(wǎng)絡,侵刪

一個簡單的事件循環(huán)的闡釋

舉個例子:

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

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

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

foo()

此代碼會如預期地打?。?/p>

foo
bar
baz

當運行此代碼時,會首先調用 foo()。 在 foo() 內部,會首先調用 bar(),然后調用 baz()。

此時,調用堆棧如下所示:


圖片來源網(wǎng)絡,侵刪

每次迭代中的事件循環(huán)都會查看調用堆棧中是否有東西并執(zhí)行它直到調用堆棧為空:


圖片來源網(wǎng)絡,侵刪

入隊函數(shù)執(zhí)行

上面的示例看起來很正常,沒有什么特別的:JavaScript 查找要執(zhí)行的東西,并按順序運行它們。

讓我們看看如何將函數(shù)推遲直到堆棧被清空。

setTimeout(() => {}, 0)的用例是調用一個函數(shù),但是是在代碼中的每個其他函數(shù)已被執(zhí)行之后。

舉個例子:

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

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

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

foo()

該代碼會打?。?/p>

foo
baz
bar

當運行此代碼時,會首先調用 foo()。 在 foo() 內部,會首先調用 setTimeout,將 bar 作為參數(shù)傳入,并傳入 0 作為定時器指示它盡快運行。 然后調用 baz()。

此時,調用堆棧如下所示:


圖片來源網(wǎng)絡,侵刪

這是程序中所有函數(shù)的執(zhí)行順序:


圖片來源網(wǎng)絡,侵刪

為什么會這樣呢?

消息隊列

當調用 setTimeout() 時,瀏覽器或 Node.js 會啟動定時器。 當定時器到期時(在此示例中會立即到期,因為將超時值設為 0),則回調函數(shù)會被放入“消息隊列”中。

在消息隊列中,用戶觸發(fā)的事件(如單擊或鍵盤事件、或獲取響應)也會在此排隊,然后代碼才有機會對其作出反應。 類似 onLoad 這樣的 DOM 事件也如此。

事件循環(huán)會賦予調用堆棧優(yōu)先級,它首先處理在調用堆棧中找到的所有東西,一旦其中沒有任何東西,便開始處理消息隊列中的東西。

我們不必等待諸如 setTimeout、fetch、或其他的函數(shù)來完成它們自身的工作,因為它們是由瀏覽器提供的,并且位于它們自身的線程中。 例如,如果將 setTimeout 的超時設置為 2 秒,但不必等待 2 秒,等待發(fā)生在其他地方。

ES6 作業(yè)隊列

ECMAScript 2015 引入了作業(yè)隊列的概念,Promise 使用了該隊列(也在 ES6/ES2015 中引入)。 這種方式會盡快地執(zhí)行異步函數(shù)的結果,而不是放在調用堆棧的末尾。

在當前函數(shù)結束之前 resolve 的 Promise 會在當前函數(shù)之后被立即執(zhí)行。

有個游樂園中過山車的比喻很好:消息隊列將你排在隊列的后面(在所有其他人的后面),你不得不等待你的回合,而工作隊列則是快速通道票,這樣你就可以在完成上一次乘車后立即乘坐另一趟車。

示例:

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

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

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  new Promise((resolve, reject) =>
    resolve('應該在 baz 之后、bar 之前')
  ).then(resolve => console.log(resolve))
  baz()
}

foo()

這會打印:

foo
baz
應該在 baz 之后、bar 之前
bar

這是 Promise(以及基于 promise 構建的 async/await)與通過 setTimeout() 或其他平臺 API 的普通的舊異步函數(shù)之間的巨大區(qū)別。
文章來源 node中文官方 http://nodejs.cn/

更多知識點 請關注:筆墨是小舟

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容