如何解釋Node.js下與瀏覽器環境代碼執行結果不一致的問題?

這段代碼在node環境下執行多次會出現以下兩種不同的結果:

第一種情況是與瀏覽器環境下行為一致的。也很好解釋,其遵循了HTML與PromiseA+規範中的多任務隊列的機制。那麼第二種情況是為何呢?個人感受為nodejs中的事件循環機制與規範如何定義的關係不大?我知道node下基於libuv實現的事件循環,裡面每次循環都會順序檢查不同的觀察者隊列如timer、idle、IO、poll、check等,那麼如何可以通過libuv的實現來解釋上面代碼的執行結果呢


在 Node 中,所有超時時間一致的 timer 都會被塞入同一個 js 構造的雙向循環鏈表裡面,這個鏈表註冊到了底層的一個 uv_timer 上去,所有可以看到超時時間一樣的 timer 在底層只映射到一個 uv_timer,這樣在大量定時器設置的場景下提升了性能。

回到題主的問題,其實這裡去翻代碼比較好解釋,在 lib/timer.js 中可以看到 setTimeout 操作會把填寫的參數構造一個 Timeout 類,這裡的 after 就是超時時間,在 node 中,setTimeout(fn, 0) 會轉化為 setTimeout(fn, 1):

function Timeout(after, callback, args) {
this._called = false;
this._idleTimeout = after;
this._idlePrev = this;
this._idleNext = this;
this._idleStart = null;
this._onTimeout = callback;
this._timerArgs = args;
this._repeat = null;
this._destroyed = false;
this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
this[trigger_async_id_symbol] = initTriggerId();
// ...
}

重點關注下這裡的 _idleStart ,因為構造後的 timeout 實例會被 insert 方法插入全局鏈表,此時會對 _idleStart 進行賦值:

item._idleStart = TimerWrap.now();

這裡的 TimerWrap.now() 方法的實現在 timer_wrap.cc 中:

static void Now(const FunctionCallbackInfo& args) {
Environment* env = Environment::GetCurrent(args);
uv_update_time(env-&>event_loop());
uint64_t now = uv_now(env-&>event_loop());
CHECK(now &>= env-&>timer_base());
now -= env-&>timer_base();
if (now &<= 0xfffffff) args.GetReturnValue().Set(static_cast&(now));
else
args.GetReturnValue().Set(static_cast&(now));
}

實際上就是 libuv 記錄的從 event loop 啟動到現在的時間,單位為 ms,所以這裡面就會產生一個現象:

因為 js 代碼執行本身也耗費時間,所以題主寫的 setTimeout1 和 setTimeout2 構造的 Timeout 實例的 _idleStart 值是可能不一樣的,而且一定是 setTimeout2._idleStart &>= setTimeout1._idleStart

好了,看到這裡答案基本上也出來了,上面提到的那個超時時間一樣的 js 鏈表註冊到 uv_timer 上面的回調函數大概邏輯如下:

function listOnTimeout() {
var list = this._list;
var msecs = list.msecs;
var now = TimerWrap.now();
var diff, timer;
while (timer = L.peek(list)) {
diff = now - timer._idleStart;
// 鏈表中的定時器尚未超時,更改此 uv_timer 的超時時間
if (diff &< msecs) { var timeRemaining = msecs - (TimerWrap.now() - timer._idleStart); if (timeRemaining &< 0) { timeRemaining = 1; } this.start(timeRemaining); debug(%d list wait because diff is %d, msecs, diff); return; } // 正常超時的定時器操作 L.remove(timer); // ... } }

當 setTimeout1 和 setTimeout2 的 _idleStart 值不一樣時,就會觸發 if 條件中的邏輯,導致 setTimeout1 先執行完,下一個 tick 再執行 setTimeout2,這就是第一種情況;

當 setTimeout1 和 setTimeout2 的 _idleStart 值一樣時,setTimeout1 和 setTimeout2 在同一個 js 定時器鏈表中,會被上面代碼中的 while 循環依次遍歷執行,這就是第二種情況;

最後需要注意的是,promise.then 註冊的回調會被 enqueue 到 v8 的 microtask queue 中,這個隊列會在 setTimeout 中的回調執行完成後立即被執行,可以看下這張圖:

參考:

* 淺探Node中定時模塊構造的JS鏈表 - CNode技術社區


碰巧最近寫過一篇相關的文章,樓主可以參考一下,https://github.com/yzfdjzwl/blog/blob/master/node/event-loop.md


推薦閱讀:

nunjucks如何在express 4中使用?
Node.js 中 setTimeout(f1, 0) 與 setImmediate(f2) ,f1 f2的執行順序是隨機的嗎,為什麼呢?
為什麼 Node.js 不給每一個.js文件以獨立的上下文來避免作用域被污染?
為什麼nodejs不給每一個.js文件以獨立的上下文來避免作用域被污染?
nodejs中,zlib.gzip系列純cpu計算函數為什麼會有非同步版本?

TAG:前端開發 | Nodejs |