在上一篇文章理解NodeJs中的Event Loop、Timers以及process.nextTick中筆者提了幾個問題,如今針對這些問題給出個人理解,若有錯漏煩請指正。html
若是你對NodeJs系列感興趣,歡迎關注微信公衆號:前端神盾局或 github NodeJs系列文章前端
在上一篇文章中提到在poll階段會「接收新的I/O事件而且在適當時node會阻塞在這裏」,那什麼狀況下會阻塞呢?阻塞多久呢?node
對於這個問題,咱們必須深刻到libuv的源碼,看看poll階段是怎麼實現的:c++
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); uv__run_timers(loop); ran_pending = uv__run_pending(loop); uv__run_idle(loop); 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); uv__run_check(loop); uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { /* UV_RUN_ONCE implies forward progress: at least one callback must have * been invoked when it returns. uv__io_poll() can return without doing * I/O (meaning: no callbacks) when its timeout expires - which means we * have pending timers that satisfy the forward progress constraint. * * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from * the check. */ uv__update_time(loop); 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; }
從源碼咱們能夠看到uv__io_poll
傳入了timeout
做爲參數,而這個timeout
就決定了poll階段阻塞的時長,明白這一點咱們就能夠把問題轉化成:是什麼決定的timeout
的值?git
再回到源碼中,timeout
的初始值爲0,也就意味着poll階段以後會直接轉入check階段而不會發生阻塞。可是當(mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT
這些條件成立時,timeout
就由uv_backend_timeout
的返回值決定。github
這裏須要插播一下關於mode
值的問題,根據官方文檔 mode
一共有三種狀況:c#
UV_RUN_DEFAULT
UV_RUN_ONCE
UV_RUN_NOWAIT
這裏咱們只關心UV_RUN_DEFAULT
,由於Node event loop使用的是這種模式.微信
OK~回到問題,咱們再看一下uv_backend_timeout
會返回什麼?異步
int uv_backend_timeout(const uv_loop_t* loop) { if (loop->stop_flag != 0) return 0; if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) return 0; if (!QUEUE_EMPTY(&loop->idle_handles)) return 0; if (!QUEUE_EMPTY(&loop->pending_queue)) return 0; if (loop->closing_handles) return 0; return uv__next_timeout(loop); }
這是一個多步條件判斷函數,咱們一個個分析:函數
uv_stop()
,stop_flag != 0
),timeout
爲0timeout
爲0idle_handles
和pending_queue
,timeout
爲0(對於idle_handles
和pending_queue
分別表明什麼,筆者尚未概念,若是後面有相應資料會及時更新)timeout
爲0uv__next_timeout
處理int uv__next_timeout(const uv_loop_t* loop) { const struct heap_node* heap_node; const uv_timer_t* handle; uint64_t diff; heap_node = heap_min((const struct heap*) &loop->timer_heap); if (heap_node == NULL) return -1; /* block indefinitely */ handle = container_of(heap_node, uv_timer_t, heap_node); if (handle->timeout <= loop->time) return 0; // 這句代碼給出了關鍵性的指導 // 對比當前loop的時間戳 diff = handle->timeout - loop->time; //不能大於最大的INT_MAX if (diff > INT_MAX) diff = INT_MAX; return diff; }
總結一下,event loop 知足如下條件時,poll階段會進行阻塞:
而阻塞的時間最長不超過給定定時器的最小閥值
setTimeout
和setImmediate
的執行順序是不必定的?上文提到setTimeout
和setImmediate
在非I/O循環中,執行順序是不必定的,好比:
setTimeout(function timeout() { console.log('timeout'); }, 0); setImmediate(function immediate() { console.log('immediate'); });
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
相同代碼,兩次運行結果倒是相反的,這是爲何呢?
在node中,setTimeout(cb, 0) === setTimeout(cb, 1)
在event loop的第一個階段(timers階段),node都會從一堆定時器中取出一個最小閥值的定時器來與loop->time
進行比較,若是閥值小於等於loop->time
表示定時器已超時,相應的回調便會執行(隨後會檢查下一個定時器),若是沒有則會進入下一個階段。
因此setTimeout
是否在第一階段執行取決於loop->time
的大小,這裏可能出現兩種狀況:
loop->time >=1
,則uv_run_timer
生效,timeout
先執行loop->time < 1
,則本次loop中的第一次uv_run_timer
不生效,那麼io_poll
後先執行uv_run_check
,即immediate
先執行,而後等close cb
執行完後,繼續執行uv_run_timer
這就是爲何同一段代碼,執行結果隨機的緣故。那爲何說在I/O回調中,必定是先immediate
執行呢,其實也很容易理解,考慮如下場景:
// timeout_vs_immediate.js const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
因爲timeout
和immediate
的事件註冊是在readFile
的回調執行時觸發的,因此必然的,在readFile
的回調執行前的每一次event loop進來的uv_run_timer
都不會有超時事件觸發
那麼當readFile
執行完畢,poll階段收到監聽的fd事件完成後,執行了該回調,此時
timeout
事件註冊immediate
事件註冊readFile
的回調執行完畢,那麼就會從uv_io_poll
中出來,此時當即執行uv_run_check
,因此immediate
事件被執行掉uv_run_timer
檢查timeout
事件,執行timeout
事件因此你會發現,在I/O回調中註冊的二者,永遠都是immediately
先執行
棧展開主要是指在拋出異常後逐層匹配catch語句的過程,舉個例子:
function a(){ b(); } function b(){ c(); } function c(){ throw new Error('from function c'); } a();
這個例子中,函數c
拋出異常,這是首先會在c
函數自己檢查是否存在try相關的catch語句,若是沒有就退出當前函數,而且釋放當前函數的內存並銷燬局部對象,繼續到b
函數中查找,這個過程就稱之爲棧展開。