以前大概看了libuv的源碼,看到eventloop裏每一個phase,一旦進入以後,在這個phase的回調隊列的全部回調被執行完以前,是不會返回的。這就和node的表現有出入了,node 11以後,每執行一個回調,都會看一看有沒有待執行 nextTick 和 microtask回調。 感受有點詭異,因此就看了一下node這邊timer的源碼。node
先看看暴露給咱用的setTimeout函數:git
function setTimeout(callback, after, arg1, arg2, arg3) {
if (typeof callback !== 'function') {
throw new ERR_INVALID_CALLBACK(callback);
}
var i, args;
switch (arguments.length) {
// fast cases
case 1:
case 2:
break;
case 3:
args = [arg1];
break;
case 4:
args = [arg1, arg2];
break;
default:
args = [arg1, arg2, arg3];
for (i = 5; i < arguments.length; i++) {
// Extend array dynamically, makes .apply run much faster in v6.0.0
args[i - 2] = arguments[i];
}
break;
}
// 初始化一個Timeout對象(雙向鏈表節點)
const timeout = new Timeout(callback, after, args, false);
active(timeout); // 插入到對應的鏈表裏
return timeout;
}
複製代碼
須要注意的是這裏生成了一個Timeout對象,那Timeout長啥樣?github
function Timeout(callback, after, args, isRepeat) {
after *= 1; // Coalesce to number or NaN
if (!(after >= 1 && after <= TIMEOUT_MAX)) {
if (after > TIMEOUT_MAX) {
process.emitWarning(`${after} does not fit into` +
' a 32-bit signed integer.' +
'\nTimeout duration was set to 1.',
'TimeoutOverflowWarning');
}
after = 1; // Schedule on next tick, follows browser behavior
}
this._idleTimeout = after; // 延遲時間
this._idlePrev = this; // 前一個Timeout指針
this._idleNext = this; // 後一個Timeout指針
this._idleStart = null;
// This must be set to null first to avoid function tracking
// on the hidden class, revisit in V8 versions after 6.2
this._onTimeout = null;
this._onTimeout = callback; // 傳進來的回調函數
this._timerArgs = args;
this._repeat = isRepeat ? after : null; // 是否須要重複
this._destroyed = false;
this[kRefed] = null;
initAsyncResource(this, 'Timeout');
}
複製代碼
能夠看到,Timeout對象在初始化的時候幹了3件事:bash
回到setTimeout,在建立好Timeout對象後,就調用了active函數把這個timeout放到了列表裏:app
function active(item) {
insert(item, true, getLibuvNow());
}
複製代碼
這裏介紹一下node對Timeout的處理,其實 internal/timers.js開頭的註釋也說的很清楚了:async
// Object maps are kept which contain linked lists keyed by their duration in
// milliseconds.
//
/* eslint-disable node-core/non-ascii-character */
//
// ╔════ > Object Map
// ║
// ╠══
// ║ lists: { '40': { }, '320': { etc } } (keys of millisecond duration)
// ╚══ ┌────┘
// │
// ╔══ │
// ║ TimersList { _idleNext: { }, _idlePrev: (self) }
// ║ ┌────────────────┘
// ║ ╔══ │ ^
// ║ ║ { _idleNext: { }, _idlePrev: { }, _onTimeout: (callback) }
// ║ ║ ┌───────────┘
// ║ ║ │ ^
// ║ ║ { _idleNext: { etc }, _idlePrev: { }, _onTimeout: (callback) }
// ╠══ ╠══
// ║ ║
// ║ ╚════ > Actual JavaScript timeouts
// ║
// ╚════ > Linked List
//
/* eslint-enable node-core/non-ascii-character */
//
// With this, virtually constant-time insertion (append), removal, and timeout
// is possible in the JavaScript layer. Any one list of timers is able to be
// sorted by just appending to it because all timers within share the same
// duration. Therefore, any timer added later will always have been scheduled to
// timeout later, thus only needing to be appended.
// Removal from an object-property linked list is also virtually constant-time
// as can be seen in the lib/internal/linkedlist.js implementation.
// Timeouts only need to process any timers currently due to expire, which will
// always be at the beginning of the list for reasons stated above. Any timers
// after the first one encountered that does not yet need to timeout will also
// always be due to timeout at a later time.
複製代碼
大概意思就是 根據延時時間的不一樣,好比如今有 m個30ms, n個50ms的setTimeout調用,node就會生成m個Timeout對象組成一個鏈表,放到一個對象裏,key爲30ms,value是這個鏈表。 同理也會生成n個Timeout組成50ms的鏈表。 這麼幹好處是啥呢, 註釋裏也說了,這麼搞的話, Timer的 插入、刪除等操做的時間複雜度都差很少是常量。 想一想也是,你們都是30ms的回調,新來的timer確定比以前就來的更晚過時麼,因此直接塞到隊尾就行了。ide
結構說清楚了再來看看insert函數,函數
function insert(item, refed, start) {
let msecs = item._idleTimeout;
if (msecs < 0 || msecs === undefined)
return;
// Truncate so that accuracy of sub-milisecond timers is not assumed.
msecs = Math.trunc(msecs);
item._idleStart = start; // 當前事件循環開始的時間,在libuv每一個時間循環開始都會更新一次
// 看一下有沒有對應的延時鏈表,沒有的話,就再建立一個
var list = timerListMap[msecs];
if (list === undefined) {
debug('no %d list was found in insert, creating a new one', msecs);
const expiry = start + msecs;
timerListMap[msecs] = list = new TimersList(expiry, msecs);
timerListQueue.insert(list);
// 若是過時時間比以前最近的過時時間還早,那就也schedule一下
if (nextExpiry > expiry) {
scheduleTimer(msecs);
nextExpiry = expiry;
}
}
......
// 把Timeout放到鏈表最後面
L.append(list, item);
}
複製代碼
首先會看一下是否是已經有對應延時的鏈表了,若是沒有,就新建一個。建好以後,直接扔到鏈表的最後。oop
這裏須要注意的是 scheduleTimer, 若是新的Timeout過時時間最近,那就要schedule這個Timeout。性能
再看看scheduleTimer函數,
void Environment::ScheduleTimer(int64_t duration_ms) {
if (started_cleanup_) return;
uv_timer_start(timer_handle(), RunTimers, duration_ms, 0);
}
複製代碼
這裏的uv_timer_start函數其實就是往libuv的timer phase註冊了一個回調,這裏咱重點關注一下傳入的回調函數RunTimers,
void Environment::RunTimers(uv_timer_t* handle) {
.... setup ...
Local<Function> cb = env->timers_callback_function();
MaybeLocal<Value> ret;
Local<Value> arg = env->GetNow();
/* This code will loop until all currently due timers will * process. It is impossible for us to end up in an * infinite loop due to how the JS-side
* /
// is structured.
do {
TryCatchScope try_catch(env);
try_catch.SetVerbose(true);
ret = cb->Call(env->context(), process, 1, &arg);
} while (ret.IsEmpty() && env->can_call_into_js());
......
}
複製代碼
這裏只須要關注到調用了env->timers_callback_function()這個函數,這個函數實際上是經過binding經過processTimers作的封裝,看一看processTimers
function processTimers(now) {
debug('process timer lists %d', now);
nextExpiry = Infinity;
let list;
let ranAtLeastOneList = false;
while (list = timerListQueue.peek()) {
if (list.expiry > now) {
nextExpiry = list.expiry;
return refCount > 0 ? nextExpiry : -nextExpiry;
}
if (ranAtLeastOneList)
runNextTicks(); // 執行nextTick回調
else
ranAtLeastOneList = true;
listOnTimeout(list, now); // 執行一個list裏全部到期的回調
}
return 0;
}
複製代碼
其實就是每次拿出過時時間最近的Timeout,看看時候到時了,到時了的話,就對這個Timeout調用listOnTimeout函數。
function listOnTimeout(list, now) {
const msecs = list.msecs;
debug('timeout callback %d', msecs);
var diff, timer;
let ranAtLeastOneTimer = false;
while (timer = L.peek(list)) {
diff = now - timer._idleStart;
// Check if this loop iteration is too early for the next timer.
// This happens if there are more timers scheduled for later in the list.
if (diff < msecs) {
list.expiry = Math.max(timer._idleStart + msecs, now + 1);
list.id = timerListId++;
timerListQueue.percolateDown(1); // 調整timerListQueue順序,把過時時間最近的放到前面去
debug('%d list wait because diff is %d', msecs, diff);
return;
}
if (ranAtLeastOneTimer)
runNextTicks();
else
ranAtLeastOneTimer = true;
// 把timer從列表中刪掉
L.remove(timer);
......
let start;
if (timer._repeat)
start = getLibuvNow();
try {
const args = timer._timerArgs;
// 執行js傳入的回調
if (args === undefined)
timer._onTimeout();
else
Reflect.apply(timer._onTimeout, timer, args);
} finally {
// 若是是須要重複的timer,從新insert進鏈表裏
if (timer._repeat && timer._idleTimeout !== -1) {
timer._idleTimeout = timer._repeat;
if (start === undefined)
start = getLibuvNow();
insert(timer, timer[kRefed], start);
} else if (!timer._idleNext && !timer._idlePrev) {
if (timer[kRefed])
refCount--;
timer[kRefed] = null;
if (destroyHooksExist() && !timer._destroyed) {
emitDestroy(timer[async_id_symbol]);
timer._destroyed = true;
}
}
}
emitAfter(asyncId);
}
....
}
複製代碼
能夠看到,和以前的邏輯差很少,就在鏈表裏一直拿出Timeout,直到拿出的Timeout還沒到過時時間,這個時候就把當前的list,在timerListQueue日後移動。 好比timerListQueue裏面原本按次序放着30ms, 50ms,70ms 3個list, 這裏就會先看30ms的list,有哪些Timeout過時了的就執行掉,而後會再看50ms和70ms的,這裏看完以後,這個時間循環階段全部的timer也就都看完了。
須要特別注意的是,在看一看processTimers和listOnTimeout裏,若是不是第一次執行,都會先調用runNextTicks這個函數,看名字就知道是幹啥的了,執行nextTick回調
這個函數實際上是封裝了processTicksAndRejections,
function processTicksAndRejections() {
let tock;
do {
while (tock = queue.shift()) {
const asyncId = tock[async_id_symbol];
emitBefore(asyncId, tock[trigger_async_id_symbol]);
if (destroyHooksExist())
emitDestroy(asyncId);
const callback = tock.callback;
if (tock.args === undefined)
callback();
else
Reflect.apply(callback, undefined, tock.args);
emitAfter(asyncId);
}
setHasTickScheduled(false);
runMicrotasks();
} while (!queue.isEmpty() || processPromiseRejections());
setHasRejectionToWarn(false);
}
複製代碼
能夠看到,每次執行完nextTickQueue裏的回調後,會調用runMicroTasks這個函數,這函數就是去執行v8的微任務隊列。
看到這裏,一開始的疑惑基本就解開了。node是把咱傳給setTimeout的回調給封裝了一下(processTimers), 封裝裏面,調用實際傳入的回調前,都會先去檢查一下nextTickQueue和microTaskQueue, 若是裏面有回調的話,得先執行完。