本文首發在github,感興趣請點擊此處javascript
nodejs是單線程執行的,同時它又是基於事件驅動的非阻塞IO編程模型。這就使得咱們不用等待異步操做結果返回,就能夠繼續往下執行代碼。當異步事件觸發以後,就會通知主線程,主線程執行相應事件的回調。java
以上是衆所周知的內容。今天咱們從源碼入手,分析一下nodejs的事件循環機制。node
首先,咱們先看下nodejs架構,下圖所示:linux
如上圖所示,nodejs自上而下分爲用戶代碼即咱們編寫的應用程序代碼、npm包、nodejs內置的js模塊等,咱們平常工做中的大部分時間都是編寫這個層面的代碼。git
膠水代碼,可以讓js調用C/C++的代碼。能夠將其理解爲一個橋,橋這頭是js,橋那頭是C/C++,經過這個橋可讓js調用C/C++。
在nodejs裏,膠水代碼的主要做用是把nodejs底層實現的C/C++庫暴露給js環境。
三方插件是咱們本身實現的C/C++庫,同時須要咱們本身實現膠水代碼,將js和C/C++進行橋接。github
nodejs的依賴庫,包括大名鼎鼎的V八、libuv。
V8: 咱們都知道,是google開發的一套高效javascript運行時,nodejs可以高效執行 js 代碼的很大緣由主要在它。
libuv:是用C語言實現的一套異步功能庫,nodejs高效的異步編程模型很大程度上歸功於libuv的實現,而libuv則是咱們今天重點要分析的。
還有一些其餘的依賴庫
http-parser:負責解析http響應
openssl:加解密
c-ares:dns解析
npm:nodejs包管理器
...npm
關於nodejs再也不過多介紹,你們能夠自行查閱學習,接下來咱們重點要分析的就是libuv。編程
咱們知道,nodejs實現異步機制的核心即是libuv,libuv承擔着nodejs與文件、網絡等異步任務的溝通橋樑,下面這張圖讓咱們對libuv有個大概的印象: windows
這是libuv官網的一張圖,很明顯,nodejs的網絡I/O、文件I/O、DNS操做、還有一些用戶代碼都是在 libuv 工做的。 既然談到了異步,那麼咱們首先概括下nodejs裏的異步事件:promise
對於網絡I/O,各個平臺的實現機制不同,linux 是 epoll 模型,類 unix 是 kquene 、windows 下是高效的 IOCP 完成端口、SunOs 是 event ports,libuv 對這幾種網絡I/O模型進行了封裝。
libuv內部還維護着一個默認4個線程的線程池,這些線程負責執行文件I/O操做、DNS操做、用戶異步代碼。當 js 層傳遞給 libuv 一個操做任務時,libuv 會把這個任務加到隊列中。以後分兩種狀況:
固然,若是以爲4個線程不夠用,能夠在nodejs啓動時,設置環境變量UV_THREADPOOL_SIZE來調整,出於系統性能考慮,libuv 規定可設置線程數不能超過128個。
先簡要介紹下nodejs的啓動過程:
以上就是 nodejs 執行一個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 回調。
ran_pending = uv__run_pending(loop);
//內部調用,用戶不care,忽略
uv__run_idle(loop);
//內部調用,用戶不care,忽略
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
//計算距離下一個timer到來的時間差。
timeout = uv_backend_timeout(loop);
//進入 輪詢 階段,該階段輪詢I/O事件,有則執行,無則阻塞,直到超出timeout的時間。
uv__io_poll(loop, timeout);
//進入check階段,主要執行 setImmediate 回調。
uv__run_check(loop);
//進行close階段,主要執行 **關閉** 事件
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
//更新當前時間戳
uv__update_time(loop);
//再次執行timers回調。
uv__run_timers(loop);
}
//判斷當前事件循環是否存活。
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids * dirtying a cache line. */
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
複製代碼
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
//取出定時器堆中超時時間最近的定時器句柄
heap_node = heap_min((struct heap*) &loop->timer_heap);
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
// 判斷最近的一個定時器句柄的超時時間是否大於當前時間,若是大於當前時間,說明還未超時,跳出循環。
if (handle->timeout > loop->time)
break;
// 中止最近的定時器句柄
uv_timer_stop(handle);
// 判判定時器句柄類型是不是repeat類型,若是是,從新建立一個定時器句柄。
uv_timer_again(handle);
//執行定時器句柄綁定的回調函數
handle->timer_cb(handle);
}
}
複製代碼
void uv__io_poll(uv_loop_t* loop, int timeout) {
/*一連串的變量初始化*/
//判斷是否有事件發生
if (loop->nfds == 0) {
//判斷觀察者隊列是否爲空,若是爲空,則返回
assert(QUEUE_EMPTY(&loop->watcher_queue));
return;
}
nevents = 0;
// 觀察者隊列不爲空
while (!QUEUE_EMPTY(&loop->watcher_queue)) {
/* 取出隊列頭的觀察者對象 取出觀察者對象感興趣的事件並監聽。 */
....省略一些代碼
w->events = w->pevents;
}
assert(timeout >= -1);
//若是有超時時間,將當前時間賦給base變量
base = loop->time;
// 本輪執行監聽事件的最大數量
count = 48; /* Benchmarks suggest this gives the best throughput. */
//進入監聽循環
for (;; nevents = 0) {
// 有超時時間的話,初始化spec
if (timeout != -1) {
spec.tv_sec = timeout / 1000;
spec.tv_nsec = (timeout % 1000) * 1000000;
}
if (pset != NULL)
pthread_sigmask(SIG_BLOCK, pset, NULL);
// 監聽內核事件,當有事件到來時,即返回事件的數量。
// timeout 爲監聽的超時時間,超時時間一到即返回。
// 咱們知道,timeout是傳進來得下一個timers到來的時間差,因此,在timeout時間內,event-loop會一直阻塞在此處,直到超時時間到來或者有內核事件觸發。
nfds = kevent(loop->backend_fd,
events,
nevents,
events,
ARRAY_SIZE(events),
timeout == -1 ? NULL : &spec);
if (pset != NULL)
pthread_sigmask(SIG_UNBLOCK, pset, NULL);
/* Update loop->time unconditionally. It's tempting to skip the update when * timeout == 0 (i.e. non-blocking poll) but there is no guarantee that the * operating system didn't reschedule our process while in the syscall. */
SAVE_ERRNO(uv__update_time(loop));
//若是內核沒有監聽到可用事件,且本次監聽有超時時間,則返回。
if (nfds == 0) {
assert(timeout != -1);
return;
}
if (nfds == -1) {
if (errno != EINTR)
abort();
if (timeout == 0)
return;
if (timeout == -1)
continue;
/* Interrupted by a signal. Update timeout and poll again. */
goto update_timeout;
}
。。。
//判斷事件循環的觀察者隊列是否爲空
assert(loop->watchers != NULL);
loop->watchers[loop->nwatchers] = (void*) events;
loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
// 循環處理內核返回的事件,執行事件綁定的回調函數
for (i = 0; i < nfds; i++) {
。。。。
}
}
複製代碼
uv__io_poll階段源碼最長,邏輯最爲複雜,能夠作個歸納,以下: 當js層代碼註冊的事件回調都沒有返回的時候,事件循環會阻塞在poll階段。看到這裏,你可能會想了,會永遠阻塞在此處嗎?
一、首先呢,在poll階段執行的時候,會傳入一個timeout超時時間,該超時時間就是poll階段的最大阻塞時間。
二、其次呢,在poll階段,timeout時間未到的時候,若是有事件返回,就執行該事件註冊的回調函數。timeout超時時間到了,則退出poll階段,執行下一個階段。
因此,咱們不用擔憂事件循環會永遠阻塞在poll階段。
以上就是事件循環的兩個核心階段。限於篇幅,timers階段的其餘源碼和setImmediate、process.nextTick的涉及到的源碼就不羅列了,感興趣的童鞋能夠看下源碼。
最後,總結出事件循環的原理以下,以上你能夠不care,記住下面的總結就行了。
細心的童鞋能夠發現,在事件循環的每個子階段退出以前都會按順序執行以下過程:
記住這個規律哦。
那麼,按照以上公式,代入網上各類有關 nodejs 事件循環的測試代碼,相信你已經可以解釋爲何會輸出那樣的結果了。若是不能,那就私信我吧~~