透過上一篇關於Node內部機制的文章,我們已經瞭解到javascript程式碼載入到Node中是執行在單執行緒下的,單仍然支援高併發的結構。那到底是怎樣的方式才能完成高併發呢?這就是今天要研究的事件迴圈(EVENT LOOP)。
關於事件迴圈恐怕每一門程式設計教材裡都會提到,但即便是很多經驗豐富的javascript開發者也不能很好地理解。按照通俗的說法,事件迴圈就是自程式啟動後一直執行的死迴圈。
名詞解釋:事件與迴圈這是兩個概念:
● 何為事件?
● 何為迴圈?
以最直觀的網頁為例,使用者在瀏覽器中檢視頁面的時候,會產生例如單擊、滑動、按鍵等一系列事件,後臺還會有AJAX執行等事件,它們一旦產生都會被加入到事件管理的佇列中,而不是停留在主執行緒中阻塞執行。
正是有這個事件佇列,才有迴圈的需求。javascript是以單執行緒的模式執行,能夠採用非同步和回撥處理事件,靠的就是瀏覽器底層利用執行緒(池)建立起來的迴圈機制。例如在Ajax發起請求的時候,會註冊一個事件,但直到ajax完成之後事件才會加入到佇列中,一旦被迴圈遍歷到,之後就會根據註冊事件所繫結的回撥,呼叫這個方法。
Node中的事件迴圈與瀏覽器採用的眾多方案不同的是,Node中處理事件迴圈只有一中固定的格式,由底層的Libuv實現,只使用一個執行緒完成。下面是來自Node官網的示意圖:
Node底層的事件迴圈分為六個階段,雖然事件是在一個迴圈被遍歷到,但所繫結的回撥則按階段加入到不同的回撥函式佇列中:
● Timers:用來處理setTimeOut和setInterval
● pending callbacks:執行延遲到下一個循的 I/O 回撥
● idle, prepare:維護內部功能
● poll:檢索新的I/O事件,執行相關的I/O回撥,此處可能產生阻塞
● check:只處理setImmediate
● close callbacks:一些close相關的事件,例如socket.on('close',...)
從上面得知poll階段可能發生阻塞,因此會影響timers中回撥的執行,這也意味著使用setTimeOut和setInterval得到的定時實際比設定的要長一點。
理論上只有一個事件佇列就可以,實際上Node在執行中存在多個事件佇列。一方面能簡化底層的設計,另一方面則是減少poll階段的阻塞對定時器的影響。這種階段式的設計,使得Node有了完善的事件迴圈機制,配合libuv的多執行緒,可以實現高併發的能力。
非同步功能的實現除了常規的事件所能繫結回撥外,Node提供了process.nextTick( )的方法讓開發者主動實現非同步操作。透過這個方法使迴圈能夠呼叫回撥函式,不過它並不屬於上面的迴圈,被註冊的回撥函式加入到nextTickQueue佇列,這個佇列裡的回撥是同樣可能產生阻塞。
不過當有遞迴的非同步操作時就只能使用setImmediate,它不會像process.nextTick那樣導致呼叫堆疊的錯誤。在有I/O相關的事件時,事件迴圈優先執行setImmediate,之後才是其他定時回撥(setTimeout)。