环境准备
工具: git/cmake/vscode(安装js和C++插件)/python
vscode :https://code.visualstudio.com/Download
编译参考:https://github.com/nodejs/node/blob/master/BUILDING.md
需要注意的是为了调试方便,需要在make命令中开启debug,如果觉得编译过程慢也可以适当调大并发数。
make -d j4
命令行:在根目录输入:
gdb node // 调试目标文件
break main // 在main里加断点
run test.js // 开始执行调试
vscode:编译完成后用vscode打开,配置编译项目,这里给出我的launch文件做参考
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/test.js"
},
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceRoot}/out/Debug/node",
"args": [
"test.js"
],
"stopAtEntry": true,
"cwd": "${workspaceRoot}",
"environment": [
],
"externalConsole": true,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
代码分析
setTimeout/setInterval
源码位置:node-master/lib/timers.js
-
setTimeout调用过程 :
setTimeout -> createSingleTimeout -> new Timeout() -> active
-
setInterval调用过程 :
setInterval -> createRepeatTimeout -> new Timeout -> active
两者最终都是使用的TimeOut对象,其区别在于repeat参数,这个参数控制当前timer是否重用。
repeat *= 1; // coalesce to number or NaN
if (!(repeat >= 1 && repeat <= TIMEOUT_MAX))
repeat = 1; // schedule on next tick, follows browser behavior
var timer = new Timeout(repeat, callback, args);
timer._repeat = repeat;
if (process.domain)
timer.domain = process.domain;
active(timer);
return timer;
active操作会将当前的timer合并到原来的lists对象上面,这个对象按照timeout的时间分组,key为过期时间,value为一个list,存放计时器对象。
function insert(item, unrefed) {
//..................
// Use an existing list if there is one, otherwise we need to make a new one.
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
}
计数器触发时会遍历对应list,逐个执行。调用过程 :
listOnTimeout -> tryOnTimeout -> ontimeout
function listOnTimeout() {
var list = this._list;
var msecs = list.msecs;
//...........................
while (timer = L.peek(list)) {
//...........................
tryOnTimeout(timer, list);
}
// If `L.peek(list)` returned nothing, the list was either empty or we have
// called all of the timer timeouts.
// As such, we can remove the list and clean up the TimerWrap C++ handle.
debug('%d list empty', msecs);
assert(L.isEmpty(list));
// 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];
}
// Do not close the underlying handle if its ownership has changed
// (e.g it was unrefed in its callback).
if (this.owner)
return;
this.close();
}
setImmediate
setImmediate则是个简单的链表
function Immediate() {
// assigning the callback here can cause optimize/deoptimize thrashing
// so have caller annotate the object (node v6.0.0, v8 5.0.71.35)
this._idleNext = null;
this._idlePrev = null;
this._callback = null;
this._argv = null;
this._onImmediate = null;
this._destroyed = false;
this.domain = process.domain;
this[async_id_symbol] = ++async_uid_fields[kAsyncUidCntr];
this[trigger_id_symbol] = initTriggerId();
if (async_hook_fields[kInit] > 0)
emitInit(this[async_id_symbol], 'Immediate', this[trigger_id_symbol], this);
}
setImmediate函数的调用过程:
setImmediate -> createImmediate -> immediateQueue.append(immediate);
触发时执行过程:
processImmediate -> tryOnImmediate -> runCallback
function processImmediate() {
var immediate = immediateQueue.head;
var tail = immediateQueue.tail;
var domain;
// Clear the linked list early in case new `setImmediate()` calls occur while
// immediate callbacks are executed
immediateQueue.head = immediateQueue.tail = null;
while (immediate) {
//..........................
immediate._callback = immediate._onImmediate;
// Save next in case `clearImmediate(immediate)` is called from callback
var next = immediate._idleNext;
tryOnImmediate(immediate, tail);
//..........................
if (immediate._idleNext)
immediate = immediate._idleNext;
else
immediate = next;
}
}
process.NextTick
源码位置:node-master/lib/internal/process/next_tick.js process.NextTick实现为一个队列,nextTick函数做的工作就是把回调函数转换成TickObject,然后进到nextTickQueue队列里面。
function nextTick(callback) {
if (typeof callback !== 'function')
throw new errors.TypeError('ERR_INVALID_CALLBACK');
if (process._exiting)
return;
var args;
switch (arguments.length) {
case 1: break;
case 2: args = [arguments[1]]; break;
case 3: args = [arguments[1], arguments[2]]; break;
case 4: args = [arguments[1], arguments[2], arguments[3]]; break;
default:
args = new Array(arguments.length - 1);
for (var i = 1; i < arguments.length; i++)
args[i - 1] = arguments[i];
}
const asyncId = ++async_uid_fields[kAsyncUidCntr];
const triggerAsyncId = initTriggerId();
const obj = new TickObject(callback, args, asyncId, triggerAsyncId);
nextTickQueue.push(obj);
++tickInfo[kLength];
if (async_hook_fields[kInit] > 0)
emitInit(asyncId, 'TickObject', triggerAsyncId, obj);
}
nextTick触发时的调用过程 :
_tickCallback(_tickDomainCallback) -> _combinedTickCallback
其中TickInfo类型用于控制每次执行callbck(可以想象成数组上有个指向两端的指针0指向头,1指向尾)
执行nextTick回调函数
function _tickDomainCallback() {
do {
while (tickInfo[kIndex] < tickInfo[kLength]) {
++tickInfo[kIndex];
const tock = nextTickQueue.shift();
const callback = tock.callback;
const domain = tock.domain;
const args = tock.args;
if (domain)
domain.enter();
// CHECK(Number.isSafeInteger(tock[async_id_symbol]))
// CHECK(tock[async_id_symbol] > 0)
// CHECK(Number.isSafeInteger(tock[trigger_id_symbol]))
// CHECK(tock[trigger_id_symbol] > 0)
emitBefore(tock[async_id_symbol], tock[trigger_id_symbol]);
// TODO(trevnorris): See comment in _tickCallback() as to why this
// isn't a good solution.
if (async_hook_fields[kDestroy] > 0)
emitDestroy(tock[async_id_symbol]);
// Using separate callback execution functions allows direct
// callback invocation with small numbers of arguments to avoid the
// performance hit associated with using `fn.apply()`
_combinedTickCallback(args, callback); //执行tick回调
emitAfter(tock[async_id_symbol]);
if (kMaxCallbacksPerLoop < tickInfo[kIndex])
tickDone();
if (domain)
domain.exit();
}
tickDone();
_runMicrotasks();
emitPendingUnhandledRejections();
} while (tickInfo[kLength] !== 0);
}
EventLoop
Event Loop 简单说就是在主程序里面轮循多个队列,取出其中的回调函数并且执行,再这个过程中实现异步,同时也通过精巧的设计平衡不同类型的任务。使得程序能够尽量无阻塞的运行下去。 代码如下:
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
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);
ran_pending = uv__run_pending(loop);
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout);//process.nextTick 和 microtask Promise
uv__run_check(loop);
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
/* UV_RUN_ONCE implies forward progress: at least one callback must have
* been invoked when it returns. uv__io_poll() can return without doing
* I/O (meaning: no callbacks) when its timeout expires - which means we
* have pending timers that satisfy the forward progress constraint.
*
* UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
* the check.
*/
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
这里只分析几个相关的过程。 uv__run_timers/uv__run_check
timer对象关联
uv__run_timers -> OnTimeout -> AsyncWrap::MakeCallback
JS 在索引kOnTimeout位置挂上脚本函数listOnTimeout
function createTimersList(msecs, unrefed) {
// Make a new linked list of timers, and create a TimerWrap to schedule
// processing for the list.
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;
}
AsyncWrap::MakeCallback中object()->Get语句会获取脚本函数listOnTimeout,index参数值设置的是常量0和JS中的kOnTimeout保持一致
v8::Local<v8::Value> cb_v = object()->Get(index);
CHECK(cb_v->IsFunction());
return listOnTimeoutMakeCallback(cb_v.As<v8::Function>(), argc, argv);
uv__run_timers执行时检查是否触发时间,如果时间到了,调用listOnTimeout脚本函数执行回调函数
immediate对象关联
uv__run_check -> CheckImmediate -> MakeCallback(node.cc)
JS中
function createImmediate(args, callback) {
// declaring it `const immediate` causes v6.0.0 to deoptimize this function
var immediate = new Immediate();
immediate._callback = callback;
immediate._argv = args;
immediate._onImmediate = callback;
if (!process._needImmediateCallback) {
process._needImmediateCallback = true; //这里在process中设置一些函数状态
process._immediateCallback = processImmediate; //在c中会在进行对应
}
immediateQueue.append(immediate);
return immediate;
}
uv__run_check->CheckImmediate->MakeCallback(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->isolate(),
env->process_object(),
env->immediate_callback_string(),
0,
nullptr,
{0, 0}).ToLocalChecked();
}
uv__run_check调用CheckImmediate,最终调用MakeCallback执行processImmediate脚本函数
processNextTick对象关联
JS中
// Used to run V8's micro task queue.
var _runMicrotasks = {};
// *Must* match Environment::TickInfo::Fields in src/env.h.
var kIndex = 0;
var kLength = 1;
process.nextTick = nextTick;
// Needs to be accessible from beyond this scope.
process._tickCallback = _tickCallback;
process._tickDomainCallback = _tickDomainCallback;
C中tick_callback_function函数设置到env上
Local<Object> process_object = env->process_object();
Local<String> tick_callback_function_key = env->tick_domain_cb_string();
Local<Function> tick_callback_function =
process_object->Get(tick_callback_function_key).As<Function>();
if (!tick_callback_function->IsFunction()) {
fprintf(stderr, "process._tickDomainCallback assigned to non-function\n");
ABORT();
}
process_object->Set(env->tick_callback_string(), tick_callback_function);
env->set_tick_callback_function(tick_callback_function);
processNextTick的调用相对复杂,它并非是直接在LOOP中调用,而是
setTimeout/setInterval执行后会立即触发一次调用
MaybeLocal<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
int argc,
Local<Value>* argv) {
CHECK(env()->context() == env()->isolate()->GetCurrentContext());
//......................
Local<Object> process = env()->process_object();
if (tick_info->length() == 0) {
tick_info->set_index(0);
return ret;
}
//------------这句-------------//
MaybeLocal<Value> rcheck =
env()->tick_callback_function()->Call(env()->context(),
process,
0,
nullptr);
// Make sure the stack unwound properly.
CHECK_EQ(env()->current_async_id(), 0);
CHECK_EQ(env()->trigger_id(), 0);
return rcheck.IsEmpty() ? MaybeLocal<Value>() : ret;
}
setImmediate执行后会立即触发一次调用
MaybeLocal<Value> MakeCallback(Environment* env,
Local<Value> recv,
const Local<Function> callback,
int argc,
Local<Value> argv[],
async_context asyncContext) {
// If you hit this assertion, you forgot to enter the v8::Context first.
CHECK_EQ(env->context(), env->isolate()->GetCurrentContext());
//..................
Environment::TickInfo* tick_info = env->tick_info();
if (tick_info->length() == 0) {
env->isolate()->RunMicrotasks();
}
// Make sure the stack unwound properly. If there are nested MakeCallback's
// then it should return early and not reach this code.
CHECK_EQ(env->current_async_id(), asyncContext.async_id);
CHECK_EQ(env->trigger_id(), asyncContext.trigger_async_id);
Local<Object> process = env->process_object();
if (tick_info->length() == 0) {
tick_info->set_index(0);
return ret;
}
//------------这句-------------//
if (env->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
return Undefined(env->isolate());
}
return ret;
}
有人把process.nextTick归结为microtask,其实不是那么回事,这里确实有runmicrotask,但那个是和process_next 文件里面的schedule函数调度microtask有关系,当tickquene空的时候会塞一个调度microtask的任务进去,这样tickquene调用起来的时候microtask也可以执行了。
Note
这里只是讨论了正常情况,在一些异常,超限回调的时候情况又会不一样。值得注意的是首次脚本调用时,LOOP 循环尚未开始,因此也会产生令人疑惑的问题。比如:
setTimeout(function(){
console.log("setTimeout")
})
setImmediate(function(){
console.log('setImmediate')
})
这里执行会出现两种结果:
setImmediate
setTimeout
//或者
setTimeout
setImmediate
每次执行的结果都可能不同,前者容易理解,uv_run_time处于LOOP的顶层,自然会早些执行。而后者这种情况就有点奇怪了。分析之后发现原因在于第一次执行脚本文件的过程在node中定义为LoadEnvironment,此时start loop的函数尚未走到。
void LoadEnvironment(Environment* env) {
//............................
// Execute the lib/internal/bootstrap_node.js file which was included as a
// static C string in node_natives.h by node_js2c.
// 'internal_bootstrap_node_native' is the string containing that source code.
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
"bootstrap_node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
if (try_catch.HasCaught()) {
ReportException(env, try_catch);
exit(10);
}
//............................
// Expose the global object as a property on itself
// (Allows you to set stuff on `global` from anywhere in JavaScript.)
global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);
// Now we call 'f' with the 'process' variable that we've built up with
// all our bindings. Inside bootstrap_node.js and internal/process we'll
// take care of assigning things to their places.
// We start the process this way in order to be more modular. Developers
// who do not like how bootstrap_node.js sets up the module system but do
// like Node's I/O bindings may want to replace 'f' with their own function.
Local<Value> arg = env->process_object();
f->Call(Null(env->isolate()), 1, &arg);
}
MainSource函数会读取js文件内容,这里可以还可以看到熟悉的global对象。f->Call语句执行脚本,这里同时也会调用NextTick。此时LOOP尚未开始,因而在这两个timer进队列后,timer创建的时间和LOOP update_time的时间可能相同也可能不同,如果程序执行的比较快,时间会相同。那么uv_run_time 检查时间的时候就会跳过去,这次执行就推迟到下一次循环。就会出现这种情况。
验证代码:
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
heap_node = heap_min((struct heap*) &loop->timer_heap);
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout > loop->time)
{
printf("so fast");
break;
}
uv_timer_stop(handle);
uv_timer_again(handle);
handle->timer_cb(handle);
}
}
测试结果
从结果中可以看出so fast出现的情况下immedate都在timeout之前。进一步推开来看,很多代码也有类似的问题:
//eg
setTimeout(function() { // setImmediate(function() {
setTimeout(function() {
console.log("setTimeout")
})
setImmediate(function() {
console.log("setImmediate")
})
})
总结一下,timeout,interval的执行时间大体是按照loop的顺序来执行,nextTcik触发执行。但是要完全确定其执行顺序,还需要结合代码分析。设计代码的总体原则是要尽量避免可能存在的block,让任务能够平均,稳定的跑下去。
👍