說到Node.js
的事件循環網上已經有了不少形形色色的文章來說述其中的原理,說的大概都是一個意思,學習了一段時間,對Node.js
事件循環有了必定的瞭解以後寫一篇博客總結一下本身的學習成果。javascript
事件循環
在筆者看來事件與循環自己就是兩個概念,事件是能夠被控件識別的操做,如按下肯定按鈕,選擇某個單選按鈕或者複選框。每一種控件有本身能夠識別的事件,如窗體的加載、單擊、雙擊等事件,編輯框(文本框)的文本改變事件。html
然而循環則是在GUI
線程中包含有一個循環,然而這個循環對於開發者和用戶來說是看不見的,只有關閉了程序以後該循環纔會結束。當用戶觸發了一個按鈕事件以後,就會產生響應的事件,這些時間被加入到一個隊列中,用戶在前臺不斷的產生事件,然然後臺也在不斷的處理這些時間,在處理的時候被加入到一個隊列中,因爲主循環中循環的存在會挨個處理這些對應的事件。java
而對於JavaScript
來說的話因爲JavaScript
是單線程的,對於一個比較耗時的操做則是使用異步的方法解決(Ajax...)。對於不一樣的異步事件來也是由不一樣的線程各司其職來處理的。node
Node.js中的事件循環
Node.js
的事件循環與瀏覽器的事件循環仍是有很大的區別的,當Node.js
啓動後,它會初始化事件輪詢;處理已提供的輸入腳本(或丟入REPL
,本文不涉及到),它可能會調用一些異步的API
函數調用,安排任務處理事件,或者調用process.nextTick()
,而後開始處理事件循環。npm
有一點是很是明確的,事件循環一樣運行在單線程環境下,JavaScript
的事件循環是依靠於瀏覽器來實現的,然而Node.js
則是依賴於Libuv
來實現的。promise
根據Node.js官方介紹,每次事件循環都包含了6個階段,對應到Libuv
源碼中的實現,以下圖所示,圖中顯示了事件循環的概述以及執行順序。瀏覽器
下面是Node.js
事件循環源代碼:網絡
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); // timers階段 uv__run_timers(loop); // I/O callbacks階段 ran_pending = uv__run_pending(loop); // idle階段 uv__run_idle(loop); // prepare階段 uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); // poll階段 uv__io_poll(loop, timeout); // check階段 uv__run_check(loop); // close callbacks階段 uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
假設事件循環進入到某一個階段,及時在這期間其餘隊列中的事件已經準備就緒,也會先將當前階段對應隊列中全部的回調方法執行完畢以後纔會繼續向下執行,結合代碼也是可以很好的理解的。不難能夠得出在事件循環系統中回調的執行順序是有跡可循的,一樣也會形成事件阻塞。異步
var fs = require("fs"); fs.readFile('input.txt', function (err, data) { if (err){ console.log(err.stack); return; } console.log(data.toString()); }); fs.readFile('test.txt', function (err, data) { if (err){ console.log(err.stack); return; } console.log(data.toString()); }); console.log("程序執行完畢");
對於整個事件循環有個一個大概的認知以後,接下來針對每一個階段進行詳細的說明。socket
該階段主要用來處理定時器相關的回調方法,當一個定時器超市後一個事件就會加入到該階段的隊列中,事件循環會跳轉至這個階段執行對應的回調方法。
定時器的回調會在觸發後儘量早的被調用,爲何要說盡量早的呢?由於實際的觸發事件可能要比預先設置的時間要長。Node.js
並不能保證timer
在預設時間到了就會當即執行,由於Node.js
對timer
的過時檢查不必定靠譜,它會受機器上其它運行程序影響,或者那個時間點主線程不空閒。
在這個階段中除了timers、setImmediate
,以及close
操做以外的大多數的回調方法都位於這個階段執行。例一個TCP socket
執行出現了一些錯誤,那麼這個回調函數會在I/O callbacks
階段來執行。名字會讓人誤解爲執行I/O
回調處理程序,然而一些常見的回調則會再poll
階段進行處理。
I/O callbacks
階段主要通過以下過程:
對於Poll
階段其主要的功能主要有兩點:
當事件循環到達poll
階段時,若是這時沒有要處理的定時器的回調方法,則會進行以下判斷:
poll
隊列不爲空,則事件循環會按照順序便利執行隊列中的回調方法,這個過程是同步的。若是poll
隊列爲空則會再次進行判斷
setImmediate()
,事件循環將結束poll
階段進入check
階段,並執行check
階段的任務隊列setImmediate()
,那麼事件循環可能會進入等待狀態,並等待新事件的產生,這也是該階段爲何被命名爲poll
的緣由。出了這些意外,該階段還會不斷的檢查是否有相關的定時器超市,若是有就會跳轉到timers
階段,而後執行對應的回調方法該階段執行setImmediate()
的回調函數。關於setImmediate
是一個比較特殊的定時器方法,setImmediate
的回調則會加入到check
隊列中,從事件循環的階段圖能夠知道,check
階段的執行順序是在poll
以後的。
通常狀況下,事件循環到達poll
階段後,就會檢查當前代碼是否調用了setImmediate
方法,這個在敘述poll
階段的時候已經有說起了,若是一個回調函數是被setImmediate
方法調用的,事件循環則會跳出poll
階段從而進入到check
階段。(這一段有點重複...)
close
階段是用來管理關閉事件,用於清理應用程序的狀態。如程序中的socket
關閉等都會加入到close
隊列中,當本輪事件結束後則會進入下一輪循環。
對於事件循環來講每一個階段都有一個任務隊列,當事件循環到達某個階段的時候,講執行該階段的任務隊列,知道隊列清空或執行的對調到達系統上限後,纔會轉入到下一個階段。當全部的階段被執行一次後,事件循環則就完成了一個tick
。
process.nextTick
這是Node.js
特有的方法,它不存在於任何瀏覽器(以及進程對象)中,process.nextTick
是一個異步的動做,而且讓這個動做在事件循環中當前階段執行完以後當即執行,也就是上面所說的tick
。
process.nextTick(() => { console.log("1") }) console.log("2") // 2 // 1
官方對於process.nextTick
有一段頗有意思的解釋:從語義角度看,setImmediate
(稍後會說到)應該比process.nextTick
先執行纔對,而事實相反,命名是歷史緣由也很難再變。
然而對於process.nextTick
來講該方法並非事件循環中的一部分,可是它的回調方法確是由事件循環調用的,該方法定義的回調方法會被加入到nextTickQueue
的隊列中。相反地,nextTickQueue
將會在當前操做完成以後當即被處理,而無論當前處於事件循環的哪一個階段。
Node.js
對process.nextTick
進行了限制,若遞歸調用process.nextTick
當倒帶nextTickQueue
最大限制以後則會拋出一個錯誤。
function nextTick (i){ while(i<9999){ process.nextTick(nextTick(i++)); } } // Maxmum call stack size exceeded nextTick(0);
既然說process.nextTick
也是存在於隊列中,那麼其執行順序也是根據程序所編寫順序執行的。
process.nextTick(() => { console.log(1) }); process.nextTick(() => { console.log(2) }); // 1 // 2
和其它回調函數同樣,process.nextTick
定義的回調也是由事件循環執行的,若是process.nextTick
的回調方法中出現了阻塞操做,後面的要執行的回調函數一樣會被阻塞。process.nextTick
會在各個事件階段之間執行,一旦執行,要直到nextTickQueue
被清空,纔會進入到下一個事件階段,因此若是遞歸調用process.nextTick
,會致使出現I/O starving
的問題,好比下面例子的readFile
已經完成,但它的回調一直沒法執行。
const fs = require('fs') const starttime = Date.now() let endtime; fs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime) }) let index = 0 function handler () { if (index++ >= 1000) return console.log(`nextTick ${index}`) process.nextTick(handler) } handler(); // nextTick 1 // nextTick 2 // ...... // nextTick 999 // nextTick 1000 // finish reading time: 170
process.nextTick() vs setImmediate()
seImmediate
方法不屬於ECMAScript
標準,而是Node.js
提出的新方法,它一樣將一個回調函數加入到事件隊列中,不一樣於setTimeout
和setInterval
,setImmediate
並不接受一個時間做爲參數,setImmediate
的事件會在當前事件循環的結尾觸發,對應的回調方法會在當前事件循環的末尾(check)執行。雖然它確實存在於某些瀏覽器中,但並未在全部瀏覽器中達到一致的行爲,所以在瀏覽器中使用時,您須要很是當心。它相似於setTimeout(fn,0)
代碼,但有時會優先於它。這裏的命名也不是最好的。
process.nextTick
中的回調在事件循環的當前階段中被當即執行。setImmediate
中的回調在事件循環的下一次迭代或tick
中被執行本質上,它們兩個的名字應該互相調換一下。process.nextTick()
的執行時機比setImmediate()
要更及時(上面有提過)。實施這項改變將致使不少npm
包沒法使用。天天都有不少新模塊被加入,這意味着每等待一天,就會有更多潛在的破壞發生。雖然他們的名字相互混淆,但將它們調換名字這種事是不會發生的(建議開發者在全部地方使用setImmediate
,這樣程序更容易讓人理解)。
仍然使用上述例子,若把nextTick
替換成setImmediate
會怎樣呢?
const fs = require('fs') const starttime = Date.now() let endtime; fs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime) }) let index = 0 function handler () { if (index++ >= 1000) return console.log(`setImmediate ${index}`) setImmediate(handler) } handler(); // setImmediate 1 // setImmediate 2 // finish reading time: 80 // ...... // setImmediate 999 // setImmediate 1000
這是由於嵌套調用的setImmediate()
回調,被排到了下一次事件循環才執行,因此不會出現阻塞。
setImmediate vs setTimeout
定時器在Node.js
和瀏覽器中的表現形式是相同的。關於定時器的一個重要的事情是,咱們提供的延遲不表明在這個時間以後回調就會被執行。它的真正含義是,一旦主線程完成全部操做(包括微任務)而且沒有其它具備更高優先級的定時器,Node.js
將在此時間以後執行回調。
setImmediate()
被設計在poll
階段結束後當即執行回調setTimeout()
被設計在指定下限時間到達後執行回調setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); // 結果一 // timeout // immediate /**--------華麗的分割線--------**/ // 結果二 // immediate // timeout
why?爲何會有兩個結果,筆者在研究這裏的時候也是有些不太明白,因而又作了第二個例子:
var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) }); // 運行N次 // immediate // timeout
setImmediate
的回調永遠先執行。雖然結論得出來了,可是這又是爲啥呢?回想一下文章上半段所敘述的事件循環。首先進入timer
階段,若是咱們的機器性能通常,那麼進入timer
階段時,1毫秒可能已通過去了(setTimeout(fn,0)
等價於setTimeout(fn,1)
),那麼setTimeout
的回調會首先執行。若是沒到一毫秒,那麼咱們能夠知道,在check
階段,setImmediate
的回調會先執行。爲何fs.readFile
回調裏設置的,setImmediate
始終先執行?由於fs.readFile
的回調執行是在poll
階段,因此,接下來的check
階段會先執行setImmediate
的回調。咱們能夠注意到,UV_RUN_ONCE
模式下,事件循環會在開始和結束都去執行timer
。
練習題
閱讀完本文章有什麼收穫呢?不如看下下面的代碼,預測一下輸出結果是什麼樣的。先不要急着看答案額...
const fs = require('fs'); console.log('beginning of the program'); const promise = new Promise(resolve => { console.log('I am in the promise function!'); resolve('resolved message'); }); promise.then(() => { console.log('I am in the first resolved promise'); }).then(() => { console.log('I am in the second resolved promise'); }); process.nextTick(() => { console.log('I am in the process next tick now'); }); fs.readFile('index.html', () => { console.log('=================='); setTimeout(() => { console.log('I am in the callback from setTimeout with 0ms delay'); }, 0); setImmediate(() => { console.log('I am from setImmediate callback'); }); }); setTimeout(() => { console.log('I am in the callback from setTimeout with 0ms delay'); }, 0); setImmediate(() => { console.log('I am from setImmediate callback'); }); // 輸出結果 // beginning of the program // I am in the promise function! // I am in the process next tick now // I am in the first resolved promise // I am in the second resolved promise // I am in the callback from setTimeout with 0ms delay // I am from setImmediate callback // ================== // I am from setImmediate callback // I am in the callback from setTimeout with 0ms delay
總結
對於本文中一些知識點任然有些模糊,懵懵懂懂,一直都在學習中,經過學習事件循環也看了一些文獻,在其中看到了這一句話:除了你的代碼,一切都是同步的
,我以爲頗有道理,對於理解事件循環頗有幫助。
Node.js
的事件循環分爲6個階段process.nextTick
不屬於事件循環,可是產生的回調會加入到nextTickQueue
setImmediate
和setTimeout
的執行順序會受到環境所影響文章略長若文章中有哪些錯誤,請在評論區指出,我會盡快作出修正。你們能夠踊躍發言共同進步,交流。