此次咱們就不要那麼多前戲,直奔主題,咱們的龍門陣正式開始。html
開局一道題,內容全靠吹。(此處應有滑稽)前端
// 文件名: index.js // 咱們儘可能模擬全部的異步場景,包括 timers、Promise、nextTick等等 setTimeout(() => { console.log('timeout 1'); }, 1); process.nextTick(() => { console.log('nextTick 1'); }); fs.readFile('./index.js', (err, data) => { if(err) return; console.log('I/O callback'); process.nextTick(() => { console.log('nextTick 2'); }); }); setImmediate(() => { console.log('immediate 1'); process.nextTick(() => { console.log('nextTick 3'); }); }); setTimeout(() => { console.log('timeout 2'); process.nextTick(() => { console.log('nextTick 4'); }); }, 100); new Promise((resolve, reject) => { console.log('promise run'); process.nextTick(() => { console.log('nextTick 5'); }); resolve('promise then'); setImmediate(() => { console.log('immediate 2'); }); }).then(res => { console.log(res); });
note: 上面的代碼執行環境是 node v10.7.0,瀏覽器的事件循環和 node 仍是有一點區別的,有興趣的能夠本身找資料看一看。node
好了,上面的代碼涉及到定時器、nextTick、Promise、setImmediate 和 I/O 操做。頭皮有點小發麻哈,你們想好答案了麼?檢查一下吧!c++
promise run nextTick 1 nextTick 5 promise then timeout 1 immediate 1 immediate 2 nextTick 3 I/O callback nextTick 2 timeout 2 nextTick 4
怎麼樣?跟本身想的同樣麼?不同的話,就聽我慢慢道來。git
在 Node.js 中,event loop 是基於 libuv 的。經過查看 libuv 的文檔能夠發現整個 event loop 分爲 6 個階段:github
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
event loop 的代碼在文件 deps/uv/src/unix/core.c
中。bootstrap
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; // 肯定 event loop 是否繼續 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); // timers 階段 ran_pending = uv__run_pending(loop); // pending callbacks 階段 uv__run_idle(loop); // idle 階段 uv__run_prepare(loop); // prepare 階段 timeout = 0; // 設置 poll 階段的超時時間,有如下狀況超時時間設爲 0,此時 poll 不會阻塞 // 1. stop_flag 不爲 0 // 2. 沒有活躍的 handles 和 request // 3. idle、pending callback、close 階段 handle 隊列不爲空 // 不然的話會將超時時間設置成距離當前時間最近的 timer 的時間 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 階段 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; }
這一小節咱們主要看看 Node 如何將咱們寫的定時器等等註冊到 event loop 中去並執行的。promise
以 setTimeout 爲例,首先咱們進到了 timers.js
這個文件中,找到了 setTimeout
函數,咱們主要關注這麼兩句:瀏覽器
function setTimeout(callback, after, arg1, arg2, arg3) { // ... const timeout = new Timeout(callback, after, args, false); active(timeout); return timeout; }
咱們看到它 new 了一個 Timeout 類,咱們順着這條線索找到了 Timeout 的構造函數:微信
function Timeout(callback, after, args, isRepeat) { // ... this._onTimeout = callback; // ... }
咱們主要關注這一句,Node 將回調掛載到了 _onTimeout
這個屬性上。那麼這個回調是在何時執行的呢?咱們全局搜一下 _onTimeout()
,咱們能夠發現是一個叫作 ontimeout
的方法執行了回調,好了,咱們開始順藤摸瓜,能夠找到這麼一條調用路徑 processTimers -> listOnTimeout -> tryOnTimeout -> ontimeout -> _onTimeout
。
最後的最後,咱們在文件的頭部發現了這麼幾行代碼:
const { getLibuvNow, setupTimers, scheduleTimer, toggleTimerRef, immediateInfo, toggleImmediateRef } = internalBinding('timers'); setupTimers(processImmediate, processTimers);
咱們一看,setupTimers
是從 internalBinding('timers')
獲取的,咱們去看一下 internalBinding
就知道這就是 js 代碼和內建模塊關聯的地方了。因而,咱們順着這條線索往下找,咱們去 src
目錄下去找叫 timers 的文件,果不其然,咱們找到一個叫 timers.cc
的文件,同時,找到了一個叫 SetupTimers
的函數。
void SetupTimers(const FunctionCallbackInfo<Value>& args) { CHECK(args[0]->IsFunction()); CHECK(args[1]->IsFunction()); auto env = Environment::GetCurrent(args); env->set_immediate_callback_function(args[0].As<Function>()); env->set_timers_callback_function(args[1].As<Function>()); }
上面的 args[1]
就是咱們傳遞的 processTimers
,在這個函數中咱們其實就完成了 processTimers
的註冊,它成功的註冊到了 node 中。
那是如何觸發的回調呢?這裏咱們首先先看到 event loop 代碼中的 timers 階段執行的函數,而後跟進去:
void uv__run_timers(uv_loop_t* loop) { struct heap_node* heap_node; uv_timer_t* handle; for (;;) { heap_node = heap_min(timer_heap(loop)); 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); uv_timer_again(handle); handle->timer_cb(handle); } }
這段代碼咱們將咱們的目光放在 handle->timer_cb(handle)
這一行,這個函數是在哪兒定義的呢?咱們全局搜一下 timer_cb
發現 uv_timer_start
中有這麼一行代碼:
handle->timer_cb = cb;
因此咱們知道是調用了 uv_timer_start
將回調函數掛載到了 handle 上。那麼 cb 又是什麼呢?其實你沿着代碼上去找就能發現其實 cb 就是 timers_callback_function
,眼熟對麼?這就是咱們上面註冊進來觸發回調的函數 processTimers
。
恍然大悟,原來是這麼觸發的回調,如今還有個問題,誰去調用的 uv_timer_start
呢?這個問題就簡單了,咱們經過源碼能夠知道是 ScheduleTimer
這個函數調用了,是否是感受很熟悉,對,這個函數就是咱們經過 internalBinding
引進來的 scheduleTimer
函數。
在這個地方就有點不同了。如今最新的 tag 版本和 github 上 node 最新的代碼是有區別的,在一次 pr 中,將 timer_wrap.cc
重構成了 timers.cc
,而且移除了 TimerWrap
類,再說下面的區別以前,先補充一下 timer
對應的數據結構:
// 這是在有 TimeWrap 的版本 // 對應的時間後面是一個 timer 鏈表 refedLists = { 1000: TimeWrap._list(TimersList(item<->item<->item<->item)), 2000: TimeWrap._list(TimersList(item<->item<->item<->item)), }; // 這是 scheduleTimer 的版本 refedLists = { 1000: TimersList(item<->item<->item<->item), 2000: TimersList(item<->item<->item<->item), };
在 TimeWrap
的版本里,js 是經過調用實例化後的 start()
函數去調用了 uv_timer_start
。
而 scheduleTimer
版本是註冊定時器的時候經過比較哪一個定時器是最近要執行的,從而將對應時間的 timerList
註冊到 uv_timer
中去。
那麼,爲何要這麼改呢?是爲了讓定時器和 Immediate 擁有更類似的行爲,也就是將單個 uv_timer_t handle 存在 Environment 上(Immediate 是有一個 ImmediateQueue,這也是個鏈表)。
這裏就只說了一個 timer,其餘的你們就本身去看看吧,順着這個思路你們確定會有所收穫的。
在加載 node 的時候,將 setTimeout、setInterval 的回調註冊到 timerList,將 Promise.resolve 等 microTask 的回調註冊到 microTasks,將 setImmediate 註冊到 immediateQueue 中,將 process.nextTick 註冊到 nextTickQueue 中。
當咱們開始 event loop 的時候,首先進入 timers 階段(咱們只看跟咱們上面說的相關的階段),而後就判斷 timerList 的時間是否到期了,若是到期了就執行,沒有就下一個階段(其實還有 nextTick,等下再說)。
接下來咱們說 poll 階段,在這個階段,咱們先計算須要在這個階段阻塞輪詢的時間(簡單點就是下個 timer 的時間),而後等待監聽的事件。
下個階段是 check 階段,對應的是 immediate,當有 immediateQueue 的時候就會跳過 poll 直接到 check 階段執行 setImmediate 的回調。
那有同窗就要問了,nextTick 和 microTasks 去哪兒了啊?別慌,聽我慢慢道來。
如今咱們有了剛剛找 timer 的經驗,咱們繼續去看看 nextTick 是怎麼執行的。
通過排查咱們能找到一個叫 _tickCallback
的函數,它不斷的從 nextTickQueue 中獲取 nextTick 的回調執行。
function _tickCallback() { let tock; do { while (tock = queue.shift()) { // ... const callback = tock.callback; if (tock.args === undefined) callback(); else Reflect.apply(callback, undefined, tock.args); emitAfter(asyncId); } tickInfo[kHasScheduled] = 0; runMicrotasks(); } while (!queue.isEmpty() || emitPromiseRejectionWarnings()); tickInfo[kHasPromiseRejections] = 0; }
咱們看到了什麼?在將 nextTick 的回調執行完以後,它執行了 runMicrotasks
。一切都真相大白了,microTasks 的執行時機是當執行完全部的 nextTick 的回調以後。那 nextTick 又是在何時執行的呢?
這就須要咱們去找 C++ 的代碼了,在 bootstrapper.cc
裏找到了 BOOTSTRAP_METHOD(_setupNextTick, SetupNextTick)
,因此咱們就要去找 SetupNextTick
函數。
void SetupNextTick(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); // ... env->set_tick_callback_function(args[0].As<Function>()); // ... }
咱們關注這一句,是否是很熟啊,跟上面 timer 同樣是吧,咱們將 __tickCallback
註冊到了 node,在 C++ 中經過 tick_callback_function
來調用這個函數。
咱們經過查看源碼能夠發現是 InternalCallbackScope
這個類調用 Close
函數的時候就會觸發 nextTixk 執行。
void InternalCallbackScope::Close() { if (closed_) return; closed_ = true; HandleScope handle_scope(env_->isolate()); // ... if (!tick_info->has_scheduled()) { env_->isolate()->RunMicrotasks(); } // ... if (!tick_info->has_scheduled() && !tick_info->has_promise_rejections()) { return; } // ... if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) { failed_ = true; } }
可能有同窗有疑問了,爲啥在執行 nextTick 上面還有 RunMicrotasks
呢?其實這是對 event loop 的優化,假如沒有 process.nextTick
就直接從 node 裏面調用 RunMicrotasks
加快速度。
如今在 node.cc
裏咱們找到了調用 Close
的地方:
MaybeLocal<Value> InternalMakeCallback(Environment* env, Local<Object> recv, const Local<Function> callback, int argc, Local<Value> argv[], async_context asyncContext) { CHECK(!recv.IsEmpty()); InternalCallbackScope scope(env, recv, asyncContext); scope.Close(); return ret; }
而 InternalMakeCallback()
則是在 async_wrap.cc
的 AsyncWrap::MakeCallback()
中被調用。
找了半天,只找到了 setImmediate 註冊時,註冊函數執行回調運行了這個函數,沒有找到 timer 的。以前由於使用的 TimeWrap,TimeWrap 繼承了 AsyncWrap,在執行回調的時候調用了 MakeCallback()
,問題是如今移除了 TimeWrap
,那是怎麼調用的呢?咱們會到 js 代碼,發現了這樣的代碼:
const { _tickCallback: runNextTicks } = process; function processTimers(now) { runNextTicks(); }
一切都明瞭了,在移除了 TimeWrap
以後,將 _tickCallback
放到了這裏執行,因此咱們剛剛在 C++ 裏找不到。
其實,每個階段執行完以後,都會去執行 _tickCallback
,只是方式可能有點不一樣。
好了,剛剛瞭解了關於 event loop 的一些狀況,咱們再來看看文章開頭的那段代碼,咱們一塊兒來分析。
首先運行 Promise 裏的代碼,輸出了 promise run,而後 promise.resolve 將 then 放入 microTasks。
這裏要提到的一點是 nextTick 在註冊以後,bootstrap 構建結束後運行SetupNextTick
函數,這時候就會清空 nextTickQueue 和 MicroTasks,因此輸出 nextTick 一、nextTick 五、promise then。
在 bootstrap 以後便進入了 event loop,第一個階段 timers,這時 timeout 1 定時器時間到期,執行回調輸出 timeout 1,timerList 沒有其餘定時器了,去清空 nextTickQueue 和 MicroTasks,沒有任務,這時繼續下階段,這時候有 immediate,因此跳過 poll,進入 check,執行 immediate 回調,輸出 immediate 1 和 immediate 2,並將 nextTick 3 推入 nextTickQueue,階段完成 immediateQueue 沒有須要處理的東西了,就去清空 nextTickQueue 和 MicroTasks 輸出 nextTick 3。
在這一輪,文件讀取完成,而且 timers 沒到期,進入 poll 階段,超時時間設置爲 timeout 2 的時間,執行回調輸出 I/O callback,而且向 nextTickQueue 推入 nextTick 2。阻塞過程當中沒有其餘的 I/O 事件,去清空 nextTickQueue 和 MicroTasks,輸出 nextTick 2。
這時候又到了 timers 階段,執行 timeout 2 的回調,輸出 timeout 2,將 nextTick 4 推入 nextTickQueue,這時 timeList 已經沒有定時器了,清空 nextTickQueue 和 MicroTasks 輸出 nextTick 4。
不知道你們懂了沒有,整個過程其實還比較粗糙,在學習過程當中也看了很多的源碼分析,可是 node 發展很快,不少分析已通過時了,源碼改變了很多,可是對於理清思路仍是頗有做用的。
各位看官若是以爲還行、OK、有點用,歡迎來我 GitHub 給個小星星,我會很舒服的,哈哈。
文 / 小烜同窗
完美無缺的祕密是:作技術,你快樂嗎?編 / 熒聲
本文已由做者受權發佈,版權屬於創宇前端。歡迎註明出處轉載本文。本文連接:https://knownsec-fed.com/2018...
想要訂閱更多來自知道創宇開發一線的分享,請搜索關注咱們的微信公衆號:創宇前端(KnownsecFED)。歡迎留言討論,咱們會盡量回復。
歡迎點贊、收藏、留言評論、轉發分享和打賞支持咱們。打賞將被徹底轉交給文章做者。
感謝您的閱讀。