setImmediate
先來看看當我們使用 setImmediate
的時候經歷了那些過程
我們先這樣用
setImmediate(fn, arg)複製程式碼
可以看到 setImmediate
接收到了 callback
, arg1
等幾個引數
exports.setImmediate = function(callback, arg1, arg2, arg3) {
if (typeof callback !== 'function') {
throw new TypeError('"callback" argument must be a function');
}
var i, args;
// 判斷傳入引數數量
switch (arguments.length) {
// 如果只有 callback 不帶其他引數的話,立即退出這裡的switch
// fast cases
case 1:
break;
case 2:
// 只有一個引數的話,設定 `args` 為有包含一個引數的陣列
args = [arg1];
break;
case 3:
args = [arg1, arg2];
break;
default:
// 引數長度超過 4 的話,遍歷之後的引數填入 `args`
args = [arg1, arg2, arg3];
for (i = 4; i < arguments.length; i++)
// 這裡也有提到在 Node 6.0.0 之後使用 `apply` 會比目前這種動態擴充套件陣列快很多
// extend array dynamically, makes .apply run much faster in v6.0.0
args[i - 1] = arguments[i];
break;
}
// 前面主要工作是引數的判斷和包裝,在這裡開始建立 `Immediate`
return createImmediate(args, callback);
};複製程式碼
前面主要工作是引數的判斷和包裝,在這裡開始建立 Immediate
createImmediate
function createImmediate(args, callback) {
// 這裡註釋提到,在使用 `const immediate` 在 6.0.0 中不能被優化
// 建立 `Immediate` 節點,並給節點賦引數, 值得注意的是 `_callback` 和 `_onImmediate` 同樣都是賦 `callback`
var immediate = new Immediate();
immediate._callback = callback;
immediate._argv = args;
immediate._onImmediate = callback;
// 設定 `process._needImmediateCallback` 標記,並給 `processImmediate ` 賦值到 `process._immediateCallback` ,用於原生模組呼叫
if (!process._needImmediateCallback) {
process._needImmediateCallback = true;
process._immediateCallback = processImmediate;
}
// `immediateQueue` 佇列連結串列中加入 immediate 節點
immediateQueue.append(immediate);
return immediate;
}複製程式碼
這裡的 createImmediate
根據接收的引數建立 immediate
,並把它加入到 immediateQueue
的佇列,線上程中設定需要執行Immediate回撥的標記。
Immediate 佇列節點
這裡用到的 Immediate
任務佇列節點的建構函式。這裡 ImmediateQueue 採用的的是一個無序連結串列。
function Immediate() {
// 直接註冊 callback 會導致優化不穩定(node v6.0.0, v8 5.0.71.35 老鐵不穩啊)
// 所以先就宣告,有個疑問,這裡是 hidden class 的問題嗎?
this._idleNext = null;
this._idlePrev = null;
this._callback = null;
this._argv = null;
this._onImmediate = null;
// 設定為當前執行緒的域
this.domain = process.domain;
}複製程式碼
processImmediate
function processImmediate() {
// 取佇列的頭尾,申明 `domain` 也就是域
var immediate = immediateQueue.head;
var tail = immediateQueue.tail;
var domain;
// 清空佇列頭尾
immediateQueue.head = immediateQueue.tail = null;
while (immediate) {
// immediate 任務的域
domain = immediate.domain;
// 如果沒有回撥就下一個
if (!immediate._onImmediate) {
immediate = immediate._idleNext;
continue;
}
if (domain)
domain.enter();
// 不是很明白這裡,之前不是給它倆都賦值了 `callback` 麼 ?
immediate._callback = immediate._onImmediate;
// 先暫存一個下一個節點,避免 `clearImmediate(immediate)` 被呼叫時被清理。
var next = immediate._idleNext;
tryOnImmediate(immediate, tail);
if (domain)
domain.exit();
// 如果有呼叫 `clearImmediate(immediate)` 的話就使用之前暫存的next,沒有的話,那就呼叫 `immediate._idleNext`
if (immediate._idleNext)
immediate = immediate._idleNext;
else
immediate = next;
}
// 判斷 immediate 佇列為空的話設定 `_needImmediateCallback ` 標誌為false
// 需要提到的是這裡的邏輯 C++ 模組中有實現
if (!immediateQueue.head) {
process._needImmediateCallback = false;
}
}複製程式碼
上面實現了 processImmediate 主要的作用是遍歷 immediateQueue
中的節點,並呼叫 tryOnImmediate
嘗試執行任務。
可以看到它被設定在 process
的 _immediateCallback
。那麼有一個疑問,他是在什麼時候被呼叫執行的?
可以看到這裡在env
全域性環境變數上設定 _immediateCallback
的的代理符號
// src/env.h
V(immediate_callback_string, "_immediateCallback")
static inline Environment* from_immediate_check_handle(uv_check_t* handle);
static inline Environment* from_destroy_ids_idle_handle(uv_idle_t* handle);
inline uv_check_t* immediate_check_handle();
inline uv_idle_t* immediate_idle_handle();
inline uv_idle_t* destroy_ids_idle_handle();複製程式碼
// src/node.cc
static void CheckImmediate(uv_check_t* handle) {
Environment* env = Environment::from_immediate_check_handle(handle);
HandleScope scope(env->isolate());
Context::Scope context_scope(env->context());
MakeCallback(env, env->process_object(), env->immediate_callback_string());
}複製程式碼
看到這裡 CheckImmediate
感覺已經快接近答案了。
tryOnImmediate
我們繼續回到 JS
function tryOnImmediate(immediate, oldTail) {
var threw = true;
try {
// 這裡是因為之前的 v8 會放棄優化帶有`try/finally`的function,所以這裡把執行函式再外接到一個小函式,small function 會得到v8優化
runCallback(immediate);
threw = false;
} finally {
// 如果執行成功並且有下一個節點
if (threw && immediate._idleNext) {
// 處理正常的話,繼續下一個
const curHead = immediateQueue.head;
const next = immediate._idleNext;
if (curHead) {
curHead._idlePrev = oldTail;
oldTail._idleNext = curHead;
next._idlePrev = null;
immediateQueue.head = next;
} else {
immediateQueue.head = next;
immediateQueue.tail = oldTail;
}
// 下一個事件迴圈中繼續處理 Immediate 任務佇列
process.nextTick(processImmediate);
}
}
}複製程式碼
前面提到為了獲得v8優化的 tryOnImmediate
在 try/finally
中將執行節點的callback放在了 runCallback
這個 small function 中。
runCallback
function runCallback(timer) {
const argv = timer._argv;
const argc = argv ? argv.length : 0;
switch (argc) {
// 這裡可以回頭看看上面開始的建立時的引數處理
case 0:
return timer._callback();
case 1:
return timer._callback(argv[0]);
case 2:
return timer._callback(argv[0], argv[1]);
case 3:
return timer._callback(argv[0], argv[1], argv[2]);
// more than 3 arguments run slower with .apply
default:
return timer._callback.apply(timer, argv);
}
}複製程式碼
好像終於把 setImmediate
的建立處理部分 ?看完了
setTimeout
這裡的引數處理和之前 setImmediate
引數處理很像
exports.setTimeout = function(callback, after, arg1, arg2, arg3) {
if (typeof callback !== 'function') {
throw new TypeError('"callback" argument must be a function');
}
var len = arguments.length;
var args;
if (len === 3) {
args = [arg1];
} else if (len === 4) {
args = [arg1, arg2];
} else if (len > 4) {
args = [arg1, arg2, arg3];
for (var i = 5; i < len; i++)
args[i - 2] = arguments[i];
}
return createSingleTimeout(callback, after, args);
};複製程式碼
createSingleTimeout
這裡開始有點不一樣了,繼續看程式碼
function createSingleTimeout(callback, after, args) {
// 嘗試轉換為 Number 或者 NaN
after *= 1;
// 如果 after 小於 1 或者 after > TIMEOUT_MAX
// after = 1
if (!(after >= 1 && after <= TIMEOUT_MAX))
after = 1;
// 根據引數建立新的 Timeout 佇列節點
var timer = new Timeout(after, callback, args);
if (process.domain)
timer.domain = process.domain;
// 加入到Timeout 佇列
active(timer);
return timer;
}複製程式碼
const TIMEOUT_MAX = 2147483647; // 2^31-1
補充一下, TIMEOUT_MAX 的值為 2^31-1,也就是我們最多可以通過 setTimeout 延遲執行大約 2147483647 ms,也就是 24 天左右。
Timeout 節點的建構函式
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;
// 這裡會和setInterval聯絡起來
this._repeat = null;
}複製程式碼
將 timeout 計時器插入計時器列表
這裡的叫做 時間輪演算法,這裡給相同 ms 級的 timeout 任務共用了一個 timeWrap,相同時間的任務分配在同一個連結串列,使計時任務的排程和新增的複雜度都是 O(1), 也達到高效複用了同一個 timeWrap。
const active = exports.active = function(item) {
insert(item, false);
};
// 計時器的排程或者重新排程的底層邏輯
// 將會新增計時器到已存在的計時器列表的末尾,或者建立新的列表
function insert(item, unrefed) {
const msecs = item._idleTimeout;
if (msecs < 0 || msecs === undefined) return;
// TimerWrap 是原生模組 timer_wrap
item._idleStart = TimerWrap.now();
const lists = unrefed === true ? unrefedLists : refedLists;
// 建立或者使用已存在的佇列
var list = lists[msecs];
if (!list) {
debug('no %d list was found in insert, creating a new one', msecs);
lists[msecs] = list = createTimersList(msecs, unrefed);
}
L.append(list, item);
assert(!L.isEmpty(list)); // list is not empty
}複製程式碼
建立 timeout 計時器列表
function createTimersList (msecs, unrefed) {
// 建立一個新的連結串列並建立一個 TimerWrap 例項來對連結串列進行排程
const list = new TimersList(msecs, unrefed);
L.init(list);
list._timer._list = list;
if (unrefed === true) list._timer.unref();
list._timer.start(msecs);
list._timer[kOnTimeout] = listOnTimeout;
return list;
}複製程式碼
TimersList
這裡的連結串列節點和之前的 Immediate 不同的地方是 this._timer = new TimerWrap()
, 這裡建立了一個新的 TimerWrap 例項。
function TimersList (msecs, unrefed) {
this._idleNext = null; // Create the list with the linkedlist properties to
this._idlePrev = null; // prevent any unnecessary hidden class changes.
this._timer = new TimerWrap();
this._unrefed = unrefed;
this.msecs = msecs;
this.nextTick = false;
}複製程式碼
TimerWrap
TimerWrap 是 Nodejs中的一個類,實現在 /src/timer_wrap.cc, 是一個 uv_timer_t
的封裝,是連線 JavaScript 和 libuv 的一個 brige。
我們先通過這個例子來看看 TimerWrap 能實現什麼功能。
const TimerWrap = process.binding('timer_wrap').Timer
const kOnTimeout = TimerWrap.kOnTimeout | 0
let timer = new TimerWrap();
timer.start(2333);
console.log('started');
timer[kOnTimeout] = function () {
console.log('2333!');
};
輸出:
started
2333 // 2.333s之後複製程式碼
在 libuv 的 uv_timer_t 實現中使用的是 最小堆 的資料結構,節點的最小判斷依據就是它的 timeout, 如果是相同 timeout 的話,則判斷兩個節點的 start_id, start_id 是一個遞增的節點計數,這樣也就保證了呼叫時序。
// deps/uv/src/unix/timer.c
static int timer_less_than(const struct heap_node* ha,
const struct heap_node* hb) {
const uv_timer_t* a;
const uv_timer_t* b;
a = container_of(ha, uv_timer_t, heap_node);
b = container_of(hb, uv_timer_t, heap_node);
if (a->timeout < b->timeout)
return 1;
if (b->timeout < a->timeout)
return 0;
/* Compare start_id when both have the same timeout. start_id is
* allocated with loop->timer_counter in uv_timer_start().
*/
if (a->start_id < b->start_id)
return 1;
if (b->start_id < a->start_id)
return 0;
return 0;
}複製程式碼
TimerWrap 原始碼
TimerWrap 作為一個連線 libuv 的 birge,所以我們容易看到在 Start 方法中呼叫了uv_timer_start,傳遞了自己的指標,第二個引數為回撥,第三個引數便是 timeout。
我們繼續看看 OnTimeout, 它的主要工作就是呼叫 key 為 kOnTimeout 的回撥,也就觸發了我們 JavaScript 層的回撥函式了。
// src/timer_wrap.cc
class TimerWrap : public HandleWrap {
...
private:
static void Start(const FunctionCallbackInfo<Value>& args) {
TimerWrap* wrap = Unwrap<TimerWrap>(args.Holder());
CHECK(HandleWrap::IsAlive(wrap));
int64_t timeout = args[0]->IntegerValue();
int err = uv_timer_start(&wrap->handle_, OnTimeout, timeout, 0);
args.GetReturnValue().Set(err);
}
static void OnTimeout(uv_timer_t* handle) {
TimerWrap* wrap = static_cast<TimerWrap*>(handle->data);
Environment* env = wrap->env();
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());
wrap->MakeCallback(kOnTimeout, 0, nullptr);
}複製程式碼
我們先回到 createTimersList
, 剛才簡單介紹的 TimerWrap ,現在,我們就能繼續愉快往下看了。
function createTimersList (msecs, unrefed) {
// 建立一個新的連結串列並建立一個 TimerWrap 例項來對連結串列進行排程
const list = new TimersList(msecs, unrefed);
L.init(list);
list._timer._list = list;
if (unrefed === true) list._timer.unref();
// 這裡設定延時
list._timer.start(msecs);
// 這裡設定延時的回撥函式, 下一步,繼續看? listOnTimeout
list._timer[kOnTimeout] = listOnTimeout;
return list;
}複製程式碼
listOnTimeout
這裡的套路到是和 processImmediate
類似
function listOnTimeout() {
var list = this._list;
var msecs = list.msecs;
// 如果 list.nextTick 為 true, 在下一個事件迴圈呼叫 listOnTimeoutNT 立即執行
if (list.nextTick) {
list.nextTick = false;
process.nextTick(listOnTimeoutNT, list);
return;
}
debug('timeout callback %d', msecs);
// 獲取當前執行時間
var now = TimerWrap.now();
debug('now: %d', now);
var diff, timer;
while (timer = L.peek(list)) {
diff = now - timer._idleStart;
// 判斷這裡的迴圈是否被過早呼叫
if (diff < msecs) {
var timeRemaining = msecs - (TimerWrap.now() - timer._idleStart);
if (timeRemaining < 0) {
timeRemaining = 0;
}
this.start(timeRemaining);
debug('%d list wait because diff is %d', msecs, diff);
return;
}
// 開始進入 timeout 邏輯
// 從連結串列中刪除當前計時器節點
L.remove(timer);
// 檢測是否從連結串列中移除
assert(timer !== L.peek(list));
// 沒有回撥函式的情況,跳到下一次迴圈
if (!timer._onTimeout) continue;
var domain = timer.domain;
if (domain) {
// 如果計數器回撥丟擲錯誤, domain 和 uncaughtException 都忽略異常,其他計時器正常執行
// https://github.com/nodejs/node-v0.x-archive/issues/2631
if (domain._disposed)
continue;
domain.enter();
}
tryOnTimeout(timer, list);
if (domain)
domain.exit();
}
// 計時器已經全部被呼叫,連結串列也已經清空,呼叫 TimerWrap 的 close 進行清理處理
debug('%d list empty', msecs);
assert(L.isEmpty(list));
this.close();
// Either refedLists[msecs] or unrefedLists[msecs] may have been removed and
// recreated since the reference to `list` was created. Make sure they're
// the same instance of the list before destroying.
// 清理
if (list._unrefed === true && list === unrefedLists[msecs]) {
delete unrefedLists[msecs];
} else if (list === refedLists[msecs]) {
delete refedLists[msecs];
}
}複製程式碼
tryOnTimeout
tryOnTimeout
和之前的 tryOnImmediate
的處理方式大體還是一樣
// 這裡和 tryOnImmediate一樣 也考慮到 v8 的優化,所以使用 small function 來執行 timer
function tryOnTimeout(timer, list) {
timer._called = true;
var threw = true;
try {
ontimeout(timer);
threw = false;
} finally {
// 如果沒丟擲錯誤,直接結束
if (!threw) return;
// 丟擲錯誤未正常執行情況下
// 為了保證執行順序,推遲列表中所有事件到下一週期。
const lists = list._unrefed === true ? unrefedLists : refedLists;
for (var key in lists) {
if (key > list.msecs) {
lists[key].nextTick = true;
}
}
// We need to continue processing after domain error handling
// is complete, but not by using whatever domain was left over
// when the timeout threw its exception.
const domain = process.domain;
process.domain = null;
// 如果丟擲錯誤,在 nextTick 中執行接下來的計數器回撥
process.nextTick(listOnTimeoutNT, list);
process.domain = domain;
}
}複製程式碼
ontimeout
function ontimeout(timer) {
var args = timer._timerArgs;
var callback = timer._onTimeout;
if (!args)
callback.call(timer);
else {
switch (args.length) {
case 1:
callback.call(timer, args[0]);
break;
case 2:
callback.call(timer, args[0], args[1]);
break;
case 3:
callback.call(timer, args[0], args[1], args[2]);
break;
default:
callback.apply(timer, args);
}
}
// 這裡就是 setInterval 的實現了,之後再細看
if (timer._repeat)
rearm(timer);
}複製程式碼
setInterval
這裡的實現和 setTimeout , setImmediate 幾乎一樣。
exports.setInterval = function(callback, repeat, arg1, arg2, arg3) {
if (typeof callback !== 'function') {
throw new TypeError('"callback" argument must be a function');
}
var len = arguments.length;
var args;
if (len === 3) {
args = [arg1];
} else if (len === 4) {
args = [arg1, arg2];
} else if (len > 4) {
args = [arg1, arg2, arg3];
for (var i = 5; i < len; i++)
// extend array dynamically, makes .apply run much faster in v6.0.0
args[i - 2] = arguments[i];
}
return createRepeatTimeout(callback, repeat, args);
};複製程式碼
interval === repeat timeout ?
setInterval
的實現和 setTimeout
不同在於 timer._repeat = repeat
function createRepeatTimeout(callback, repeat, args) {
repeat *= 1; // coalesce to number or NaN
if (!(repeat >= 1 && repeat <= TIMEOUT_MAX))
repeat = 1; // schedule on next tick, follows browser behaviour
var timer = new Timeout(repeat, callback, args);
timer._repeat = repeat;
if (process.domain)
timer.domain = process.domain;
active(timer);
return timer;
}複製程式碼
clear
之前看了建立 3 種時間排程的方法,在看看清理的 timer 的程式碼。
clearImmediate
exports.clearImmediate = function(immediate) {
if (!immediate) return;
immediate._onImmediate = null;
immediateQueue.remove(immediate);
if (!immediateQueue.head) {
process._needImmediateCallback = false;
}
};複製程式碼
clearTimeout
const clearTimeout = exports.clearTimeout = function(timer) {
if (timer && (timer[kOnTimeout] || timer._onTimeout)) {
timer[kOnTimeout] = timer._onTimeout = null;
if (timer instanceof Timeout) {
timer.close(); // for after === 0
} else {
unenroll(timer);
}
}
};複製程式碼
Timeout.unref
這裡的 timer 提供了 close
,unref
,ref
3 個方法,其中 ref
和 unref
通過 TimerWrap
呼叫底層的 uv_ref()
和 uv_unref()
。
在 Nodejs 官方文件提到
When called, the active Timeout object will not require the Node.js event loop to remain active. If there is no other activity keeping the event loop running, the process may exit before the Timeout object's callback is invoked.
主動呼叫 unref()
,如果沒有其他活躍的物件,可能會使 Nodejs 的事件迴圈提前退出
Timeout.prototype.unref = function() {
if (this._handle) {
this._handle.unref();
} else if (typeof this._onTimeout === 'function') {
var now = TimerWrap.now();
if (!this._idleStart) this._idleStart = now;
var delay = this._idleStart + this._idleTimeout - now;
if (delay < 0) delay = 0;
// 防止在呼叫 `unref()`之後 再次執行回撥
if (this._called && !this._repeat) {
unenroll(this);
return;
}
var handle = reuse(this);
this._handle = handle || new TimerWrap();
this._handle.owner = this;
this._handle[kOnTimeout] = unrefdHandle;
this._handle.start(delay);
this._handle.domain = this.domain;
this._handle.unref();
}
return this;
};複製程式碼
Timeout.ref
Timeout.prototype.ref = function() {
if (this._handle)
this._handle.ref();
return this;
};複製程式碼
Timeout.close
Timeout.prototype.close = function() {
this._onTimeout = null;
if (this._handle) {
this._idleTimeout = -1;
this._handle[kOnTimeout] = null;
this._handle.close();
} else {
unenroll(this);
}
return this;
};
// 移除計時器,取消延時以及重置有關的計時器屬性
const unenroll = exports.unenroll = function(item) {
var handle = reuse(item);
if (handle) {
debug('unenroll: list empty');
handle.close();
}
// 確保之後不會被繼續插入佇列
item._idleTimeout = -1;
};
// 為了複用 TimerWrap 的一簡單的轉換函式
//
// This mostly exists to fix https://github.com/nodejs/node/issues/1264.
// Handles in libuv take at least one `uv_run` to be registered as unreferenced.
// Re-using an existing handle allows us to skip that, so that a second `uv_run`
// will return no active handles, even when running `setTimeout(fn).unref()`.
function reuse(item) {
L.remove(item);
var list = refedLists[item._idleTimeout];
// if empty - reuse the watcher
if (list && L.isEmpty(list)) {
debug('reuse hit');
list._timer.stop();
delete refedLists[item._idleTimeout];
return list._timer;
}
return null;
}複製程式碼
clearInterval
exports.clearInterval = function(timer) {
if (timer && timer._repeat) {
timer._repeat = null;
clearTimeout(timer);
}
};複製程式碼
結尾 ?
先上圖
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘複製程式碼
setImmediate 一般在 check 階段執行,也有可能在 poll 階段執行
setTimeout setInterval 在 timer 階段執行
來一個問題:setTimeout(fn, 0)
setImmediate(fn)
誰會先執行?
setTimeout(console.log, 0, 1);
setImmediate(console.log, 2);
// event loop 每個階段都比較空閒的話,一次 event loop 小於 1ms 時:
2
1
// 超過 1ms 時也可能是
1
2複製程式碼
如果在一個I/O迴圈內呼叫,immediate 始終會比 setTimeout 先執行。因為immediate 會在 event loop 中 poll 完成之後立即執行,setTimeout 則是到下一個 timers 階段。
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(console.log, 0, 1);
setImmediate(console.log, 2);
})
// 輸出:
2
1複製程式碼
再來一個
我們在 Nodejs 中這樣寫, 會怎麼輸出?
var a = setTimeout(console.log, 50, 2333);
a._repeat = true;複製程式碼
這樣呢?
var a = setTimeout(console.log, 1000, 2333);
a.close()複製程式碼
這樣呢?
var a = setTimeout(console.log, 1000, 2333);
a.unref()複製程式碼
參考資料:
node/lib/internal/linkedlist.js