【譯】Node.js中的Event Loop

原文連接: flaviocopes.com/node-event-…javascript

指南

爲了瞭解Node,Event Loop(後面我會翻譯成「事件循環」)是其中最重要的方面。java

爲何它如此重要?由於它代表了Node是怎樣作到異步而且擁有非堵塞的I/O操做,固然也是使得Node的「殺手級」應用得以成功的重要緣由。node

Node.js的代碼在單線程上運行。也就是每個時刻只會發生一件事情。web

這是一種限制,實際上卻很是有用,在很大程度上簡化了你的應用程序而不須要擔憂併發的問題。api

你只須要關心如何去編寫你的代碼,規避任何會堵塞你線程的東西。好比說同步的網絡調用以及無限循環promise

一般,在大多數的瀏覽器中,每個瀏覽器的Tab都有一個事件循環,這使得每個處理過程相互隔離,避免網頁陷入無限循環或者繁重的處理過程當中的時候會堵塞住整個瀏覽器。瀏覽器

特殊的瀏覽器環境管理着多個同時運行的事件循環,好比處理Api的調用。Web Workers也是運行在它們本身的事件循環中。bash

你主要須要在乎的是你的代碼將會運行在單個事件循環上,編寫代碼的時候把它放在心上,避免堵塞它。網絡

堵塞事件循環

任何JavaScript代碼若是花費太長的時間纔可以把控制權歸還給事件循環的話,那將會堵塞頁面中其餘JavaScript代碼的執行,甚至堵塞UI線程,使得用戶不可以點擊,滾動頁面等等。併發

在JavaScript裏面幾乎全部原始的I/O操做都是是非堵塞的。如網絡請求,文件系統操做等等。通常異常狀況下才會被堵塞,這也是JavaScript裏面有這麼多的回調函數的緣由,也包括最近出現的promises以及async/await

調用棧

調用棧是一個LIFO的隊列(後進先出)。

事件循環會持續地檢查調用棧,看看是否有須要被執行的函數。

在這個過程當中,它會把從調用棧找到的全部函數添加進來,並依次調用它們。

你可能已經熟悉在調試工具或者瀏覽器console裏面出現的錯誤棧跟蹤信息了吧?瀏覽器從調用棧中查找函數名,而後告訴你哪一個函數是當前調用的起源:

error stack

關於事件循環的簡單描述

讓咱們找個例子:

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

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

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

foo()
複製代碼

代碼的打印結果是

foo
bar
baz
複製代碼

跟預期的同樣,代碼運行的時候首先調用foo函數,接下來在foo內部bar函數將被調用,最後再調用函數baz

該過程當中調用棧看起來像這樣。

call stack

事件循環在每次迭代中都會查看調用棧中是否有東西,有的話就並執行它:

each interation

直到調用棧爲空。

隊列中函數的執行

上面的例子看起來很正常,沒有任何特別的東西:JavaScript尋找一些須要執行的事物,並依次執行它們。

接下來讓咱們看看如何推遲一個函數的執行,直到調用棧爲空才執行該函數。

案例setTimeout(() => {}), 0)將會喚起一個函數,可是這個函數將會等到代碼中的其餘函數都被執行完以後纔會運行。

舉個例子:

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

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

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

foo()
複製代碼

這段代碼的打印結果可能有點出乎意料:

foo
baz
bar
複製代碼

當這段代碼運行的時候,首先函數foo被調用,在foo內部首先會調用setTimeout,這裏傳入bar函數來做爲它的第一個參數,另外爲了讓它可以儘快執行,傳入參數0做爲計時器的過時時間。接下來再調用baz函數。

此時調用棧看起來像這樣:

call stack with setTimeout

下面是咱們的程序中全部函數的執行順序:

execution order with setTimeout

爲何會發生這種事情?

消息隊列

setTimeout被調用時,瀏覽器或者Node.js會開啓一個計時器。一旦計時器過時,回調函數就會被加入到消息隊列中。而在這個例子中由於咱們設置了0做爲超時時間,因此函數將會立刻被加入到消息隊列

消息隊列是用戶發起的事件,如點擊事件或者鍵盤事件存活的地方。fetch的響應在可以被你代碼使用以前也被放置於隊列中。又或者是像onLoad那樣的DOM事件。

事件循環給予調用棧較高的優先級,首先它會處理全部可以在調用棧中找到的函數,一旦調用棧爲空,它就開始從消息隊列中選取函數。

咱們的程序不須要停下來等待像setTimeoutfetch或者其餘一些相似的函數直到它們完成工做,由於它們是瀏覽器提供的功能,而且存活在他們本身的線程中。舉個例子,若是你設置了setTimeout的超時時間爲2秒,你並不須要真的停下來等待兩秒後才執行後續的代碼-這個等待將會在其餘地方進行。

ES6工做隊列

ECMAScript 2015提出了一個叫作工做隊列的概念,Promises將會運用這個隊列。這是一個儘量快速地執行異步函數的方式,而不是把異步函數放置在調用棧的最後面。

Promises將在當前函數結束以前被解析,且將在當前函數以後被執行。

我找到一個很好的類比,就是娛樂公園的過山車。消息隊列就像是把你放在隊列以後,排在全部人的後面,你必需要等待你那個回合的到來。工做隊列就像是一個快速通行證,在你完成上一個項目以後你就能夠立刻開始下一次的乘坐。

例子:

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()
複製代碼

打印結果是

foo
baz
should be right after baz, before bar
bar
複製代碼

這是Promises(以及構建於promises之上的async/await)與舊的經過setTimeout或者其餘平臺API的異步方式之間比較大的不一樣。

結論

這篇文章爲你介紹了關於Node.js事件循環的基本組成部分。

它是任何經過Node.js編寫的程序的基本部分,我但願在這裏闡述的一些概念在未來會對你有所幫助。


閱讀我全部的Node.js教程

相關文章
相關標籤/搜索