以前大概看了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, 若是里面有回调的话,得先执行完。