杰克-逊の黑豹,恰饭了啦 []( ̄▽ ̄)
前言
本文是 好激动,离nodejs事件循环更近一步 的续篇。透过node执行一个js脚本的情况,来说说promise.then, process.nextTick都发生了什么事情。一些基础铺垫不再赘述,如果有些朋友理解本文遇到一些问题的话,可以阅读上篇文章。
如果没有特殊说明,文章中给出的代码都是出自node源码。
阅读cpp代码内容,直接看带有注释的代码行即可,其余地方的代码没那么重要,不影响理解哦。
js脚本执行情况简述
js
// test.js
console.log('A');
process.nextTick(() => {
console.log('B');
});
process.nextTick(async () => {
console.log('C');
});
Promise.resolve().then(() => {
console.log('D');
});
setTimeout(() => {
console.log('E');
}, 0);
setImmediate(() => {
console.log('F');
});
cpp
// src/node_main_instance.cc
int NodeMainInstance::Run(const EnvSerializeInfo* env_info) {
// ...
int exit_code = 0;
DeleteFnPtr<Environment, FreeEnvironment> env = CreateMainEnvironment(&exit_code, env_info);
CHECK_NOT_NULL(env);
{
Context::Scope context_scope(env->context());
if (exit_code == 0) {
// 执行js脚本
LoadEnvironment(env.get(), StartExecutionCallback{});
// 进入事件循环
exit_code = SpinEventLoop(env.get()).FromMaybe(1);
}
ResetStdio();
}
// ...
return exit_code;
}
node执行 test.js
,是在 LoadEnvironment
函数里完成的,之后根据调试test.js
的现象,发现程序会跳入processTicksAndRejections
函数,没有立即执行 SpinEventLoop
函数,陷入到事件循环。
该函数位于
lib/internal/process/task_queues.js
)
在processTicksAndRejections
函数中,process.nextTick
注册的回调先执行,全部执行完毕后,才会执行Promise.resolve().then
注册的回调。接着,程序会走进事件循环,来到timers阶段,执行setTimeout
注册的回调,最后来到check阶段,执行setImmediate
注册的回调。
这就是node执行的基本现象。接下来,我们分成3个问题,解释下:
- 为什么脚本执行之后,没有立即进入事件循环,而是跳入
processTicksAndRejections
函数? - 为什么先执行process.nextTick注册的回调,然后再执行promise.then回调?
- process.nextTick setTimeout setImmediate setInterval 都有各自的队列嘛?
为什么脚本执行之后,没有立即进入事件循环,而是跳入processTicksAndRejections
函数?
问题出在LoadEnvironment内的StartExecution
函数。
cpp
// src/api/environment.cc
MaybeLocal<Value> LoadEnvironment(
Environment* env,
StartExecutionCallback cb) {
env->InitializeLibuv();
env->InitializeDiagnostics();
return StartExecution(env, cb);
}
cpp
// src/node.cc
MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
// 为什么脚本执行完毕后,会跳入 processTicksAndRejections 函数,
// 原因就在 InternalCallbackScope
InternalCallbackScope callback_scope(
env,
Object::New(env->isolate()),
{ 1, 0 },
InternalCallbackScope::kSkipAsyncHooks);
if (cb != nullptr) {
EscapableHandleScope scope(env->isolate());
if (StartExecution(env, "internal/bootstrap/environment").IsEmpty())
return {};
StartExecutionCallbackInfo info = {
env->process_object(),
env->native_module_require(),
};
return scope.EscapeMaybe(cb(info));
}
if (env->worker_context() != nullptr) {
return StartExecution(env, "internal/main/worker_thread");
}
std::string first_argv;
// 我们执行的是 node test.js,
// first_argv就是 "test.js"
if (env->argv().size() > 1) {
first_argv = env->argv()[1];
}
if (first_argv == "inspect") {
return StartExecution(env, "internal/main/inspect");
}
if (per_process::cli_options->print_help) {
return StartExecution(env, "internal/main/print_help");
}
if (env->options()->prof_process) {
return StartExecution(env, "internal/main/prof_process");
}
// -e/--eval without -i/--interactive
if (env->options()->has_eval_string && !env->options()->force_repl) {
return StartExecution(env, "internal/main/eval_string");
}
if (env->options()->syntax_check_only) {
return StartExecution(env, "internal/main/check_syntax");
}
if (!first_argv.empty() && first_argv != "-") {
// 命中的是这个分支,见文知意,印证了我们断点调试test.js的时候,
// 程序会经过 internal/main/run_main_module.js;
//
// 该函数执行后,test.js就被执行了;
//
// internal/main/run_main_module.js没有一行代码表明,脚本执行后,需要
// 先执行 processTicksAndRejections,因此问题在别处
return StartExecution(env, "internal/main/run_main_module");
}
if (env->options()->force_repl || uv_guess_handle(STDIN_FILENO) == UV_TTY) {
return StartExecution(env, "internal/main/repl");
}
return StartExecution(env, "internal/main/eval_stdin");
}
看过注释,我们知道问题在于InternalCallbackScope
,这里边有黑魔法!
cpp
// src/api/callback.cc
InternalCallbackScope::~InternalCallbackScope() {
Close();
env_->PopAsyncCallbackScope();
}
void InternalCallbackScope::Close() {
// ....
TickInfo* tick_info = env_->tick_info();
// ....
HandleScope handle_scope(isolate);
Local<Object> process = env_->process_object();
if (!env_->can_call_into_js()) return;
// 就是这里了!
// tick_callback指代的就是 processTicksAndRejections!
Local<Function> tick_callback = env_->tick_callback_function();
CHECK(!tick_callback.IsEmpty());
// 执行 processTicksAndRejections
if (tick_callback->Call(context, process, 0, nullptr).IsEmpty()) {
failed_ = true;
}
perform_stopping_check();
}
魔法就是,借助InternalCallbackScope
的析构函数,执行processTicksAndRejections
!
上一篇文章说过,有了V8的帮助,cpp端可以调用js端定义的函数,反之亦然。这里就是第一种情形。我知道,这样的结果还不够,因为我们不知道cpp哪行代码绑定了processTicksAndRejections
。
cpp
// src/node_task_queue.cc
// 该函数会在node执行test.js之前的初始化阶段被调用
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
// ....
env->SetMethod(target, "enqueueMicrotask", EnqueueMicrotask);
// 这一步就是注册的环节,将 js端 setTickCallback 函数 和 cpp
// 端定义的SetTickCallback函数绑定。
//
// 因为是在cpp端实现的,所以js端必定会使用它,也就是说,在js端
// 可能存在一处代码,调用了 setTickCallback
env->SetMethod(target, "setTickCallback", SetTickCallback);
env->SetMethod(target, "runMicrotasks", RunMicrotasks);
target->Set(env->context(),
FIXED_ONE_BYTE_STRING(isolate, "tickInfo"),
env->tick_info()->fields().GetJSArray()).Check();
// ....
}
static void SetTickCallback(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsFunction());
// 光看函数名,是不是猜到了什么呢?
// 这一步做了函数设置,然后调用env->tick_callback_function()
// 就会拿到该函数
env->set_tick_callback_function(args[0].As<Function>());
}
好了,现在剩下一个问题就是,在哪一步调用setTickCallback
?
搜索js端代码,我们找到了关键线索:
js
// lib/internal/process/task_queues.js
const {
tickInfo,
runMicrotasks,
setTickCallback,
enqueueMicrotask
} = internalBinding('task_queue');
module.exports = {
setupTaskQueue() {
listenForRejections();
// 出现了!
// 如果我们知道在哪里调用了 setupTaskQueue,
// 就知道 setTickCallback 在何时被触发啦
setTickCallback(processTicksAndRejections);
return {
nextTick,
runNextTicks
};
},
queueMicrotask
};
接着在js端代码全局搜索setupTaskQueue
, 发现:
js
// lib/internal/bootstrap/node.js
const {
setupTaskQueue,
queueMicrotask
} = require('internal/process/task_queues');
// ...
// dang dang!
// 这里调用 setupTaskQueue, 完成了 cpp 端对 processTicksAndRejections的绑定
// 开心!
const { nextTick, runNextTicks } = setupTaskQueue();
process.nextTick = nextTick;
// ...
但我们不能高兴太早,因为在调试test.js整个过程中,并没有发现lib/internal/bootstrap/node.js
的踪迹,没有一次程序是在此文件中停顿的。
似乎陷入僵局,但是我们还有一线希望。
上述分析,我们其实是从env->tick_callback_function
出发的,关注点放在tick_callback_function
上面,但env
有没有什么古怪呢?
从调用env的地方,回溯cpp代码,找到了env诞生之处:
cpp
// src/node.cc
int Start(int argc, char** argv) {
InitializationResult result = InitializeOncePerProcess(argc, argv);
if (result.early_return) {
return result.exit_code;
}
{
Isolate::CreateParams params;
const std::vector<size_t>* indexes = nullptr;
const EnvSerializeInfo* env_info = nullptr;
bool force_no_snapshot =
per_process::cli_options->per_isolate->no_node_snapshot;
if (!force_no_snapshot) {
v8::StartupData* blob = NodeMainInstance::GetEmbeddedSnapshotBlob();
if (blob != nullptr) {
params.snapshot_blob = blob;
indexes = NodeMainInstance::GetIsolateDataIndexes();
// 就是这里,就是这里,env对象的信息来源于此,
// 我们需要调试node源码,用lldb在此处打上
// 断点,然后 step in 到 GetEnvSerializeInfo 函数
env_info = NodeMainInstance::GetEnvSerializeInfo();
}
}
uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);
NodeMainInstance main_instance(
¶ms,
uv_default_loop(),
per_process::v8_platform.Platform(),
result.args,
result.exec_args,
indexes);
result.exit_code = main_instance.Run(env_info);
}
TearDownOncePerProcess();
return result.exit_code;
}
调试源码,跳入到NodeMainInstance::GetEnvSerializeInfo()
函数时,通过lldb提供的调试信息,发现此函数位于node_snapshot.cc
,但实际上,node源码里没有这个文件。
what? 无中生有可还行!
别慌,我们利用lldb打印了一部分 node_snapshot.cc
的注释信息,然后全文检索,竟然找到了一个匹配的文件:
cpp
// tools/snapshot/snapshot_builder.cc
std::string SnapshotBuilder::Generate(
const std::vector<std::string> args,
const std::vector<std::string> exec_args) {
// ...
std::string result;
{
std::vector<size_t> isolate_data_indexes;
EnvSerializeInfo env_info;
const std::vector<intptr_t>& external_references =
NodeMainInstance::CollectExternalReferences();
SnapshotCreator creator(isolate, external_references.data());
Environment* env;
{
main_instance = NodeMainInstance::Create(
isolate,
uv_default_loop(),
per_process::v8_platform.Platform(),
args,
exec_args);
// 创建了js的一个执行环境
env = new Environment(
main_instance->isolate_data(),
context,
args,
exec_args,
nullptr,
node::EnvironmentFlags::kDefaultFlags,
{});
// Run scripts in lib/internal/bootstrap/
{
TryCatch bootstrapCatch(isolate);
// RunBootstrapping是可以看到源码的,
// 该函数负责运行 lib/internal/bootstrap/node.js,
// 这个文件是不是很眼熟?
// 没错, setupTaskQueue 函数就是在该文件
//
// 执行之后,会触发 setupTaskQueue,
// 进而触发 setTickCallback,
// 然后触发 cpp 端的 SetTickCallback 函数,
// SetTickCallback函数内部的 env 指的就是 env->RunBootstrapping()的 env,
// 这样processTicksAndRejections就绑定在env对象上了,
// 访问 env->tick_callback_function()就能拿到
//
//
v8::MaybeLocal<Value> result = env->RunBootstrapping();
// ...
}
// ...
// 将env序列化,目的是在写入到 node_snapshot.cc,将其编译到node里,
// node就不用每次执行js脚本的时候,都先执行一遍lib/internal/bootstrap/node.js,
env_info = env->Serialize(&creator);
// ...
}
// ...
// node_snapshot.cc里面的内容和result字符串的值一致!
// 结合此文件的原本注释,获悉 node_snapshot.cc是编译时生成的,
// 并且被编译到node里面;
//
// 上面说的NodeMainInstance::GetEnvSerializeInfo()函数,返回的值就是
// 本函数中的 env_info !
//
// result的内容会被写入到 node_snapshot.cc,而node_snapshot.cc
// 会参与到编译node的过程中,透过NodeMainInstance::GetEnvSerializeInfo()
// 将env_info返回给上层函数,上层函数使用 env_info 去执行我们的test.js
// 文件,那么 env->tick_callback_function() 拿到的就是 processTicksAndRejections
//
// 因此,setupTaskQueue是什么时候被调用的呢?是在编译node的时候。
//
result = FormatBlob(&blob, isolate_data_indexes, env_info);
delete[] blob.data;
}
per_process::v8_platform.Platform()->UnregisterIsolate(isolate);
return result;
}
那么,把以上的东西串接起来,就得到最开始的问题的答案:
- 编译node阶段,将 processTicksAndRejections 绑定到env;
- test.js脚本执行后,
InternalCallbackScope
的析构函数会执行; - 析构函数内部会由env->tick_callback_function()拿到processTicksAndRejections函数,并执行;
- 最终,test.js执行完毕后,会跳入
processTicksAndRejections
函数;
为什么先执行process.nextTick注册的回调,然后再执行promise.then回调?
在nextTick回调被执行的时候,会跳到processTicksAndRejections
函数:
js
function processTicksAndRejections() {
let tock;
do {
// nextTick注册回调,回调就被存储在 queue 中,
// 遍历执行每个回调,直到队列清空
while (tock = queue.shift()) {
const asyncId = tock[async_id_symbol];
emitBefore(asyncId, tock[trigger_async_id_symbol], tock);
try {
// 拿到回调函数
const callback = tock.callback;
// 回调函数没有入参,直接执行
if (tock.args === undefined) {
callback();
} else {
const args = tock.args;
switch (args.length) {
// 回调函数有1个入参
case 1: callback(args[0]); break;
case 2: callback(args[0], args[1]); break;
case 3: callback(args[0], args[1], args[2]); break;
case 4: callback(args[0], args[1], args[2], args[3]); break;
default: callback(...args);
}
}
} finally {
if (destroyHooksExist())
emitDestroy(asyncId);
}
emitAfter(asyncId);
}
// queue 清空之后,才执行微任务,
// 因此 nextTick 回调执行时机早于 promise.then回调
runMicrotasks();
// 不过,微任务也可能调用 process.nextTick,导致queue又有数据了,
// 这时候,再次陷入循环,而不是进入事件循环的下一个阶段
} while (!queue.isEmpty() || processPromiseRejections());
setHasTickScheduled(false);
setHasRejectionToWarn(false);
}
process.nextTick setTimeout setImmediate setInterval 都有各自的队列嘛?
上个问题,已经说明,process.nextTick拥有专属队列。nodejs官网文档里也提到nextTick有专门的队列。
想知道setTimeout有没有专属队列,在调试的时候,看一下它的定义就好了:
js
// lib/timers.js
function setTimeout(callback, after, arg1, arg2, arg3) {
// ...
const timeout = new Timeout(callback, after, args, false, true);
// 关键是这里哦
insert(timeout, timeout._idleTimeout);
return timeout;
}
js
// lib/internal/timers.js
// getLibuvNow在cpp端实现,js端调用,可以拿到事件循环中计算的当前时刻
function insert(item, msecs, start = getLibuvNow()) {
// Truncate so that accuracy of sub-millisecond timers is not assumed.
msecs = MathTrunc(msecs);
item._idleStart = start;
// Use an existing list if there is one, otherwise we need to make a new one.
let 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);
if (nextExpiry > expiry) {
scheduleTimer(msecs);
nextExpiry = expiry;
}
}
// item 插入到 list 中,而 list 存储在
// timerListQueue,为了快速访问list, list
// 又被存储在了 timerListMap
//
// 因此 timerListQueue 就是setTimeout的专属队列
L.append(list, item);
}
setTimeout也有专属队列,没跑了。而setInterval共用了setTimeout队列。
js
// lib/timers.js
function setInterval(callback, repeat, arg1, arg2, arg3) {
// ...
// 和 setTimeout 的区别在于第四个参数是 true,不是false
const timeout = new Timeout(callback, repeat, args, true, true);
// insert也是同一个,因此队列也是一样的
insert(timeout, timeout._idleTimeout);
return timeout;
}
setImmediate的情况:
js
// lib/timers.js
function setImmediate(callback, arg1, arg2, arg3) {
// ...
return new Immediate(callback, args);
}
js
// lib/internal/timers.js
class Immediate {
constructor(callback, args) {
// ...
// 出乎意料的简单,它也有专属队列哦
immediateQueue.append(this);
}
}
结语
洋洋洒洒写了这么多,腰都疼了。整体上大致串了一趟,希望可以帮助更多朋友理解事件循环。不过,只讲了promise执行的时机,但是创建promise方面还没有说到,有些意犹未尽,之后有时间再补充一篇文章吧。
毕竟只是一种分享性质的文章,希望各位朋友读后仅仅作为个人学习使用,不要用在面试候选人身上,否则,我写的东西就是毒药了😭。
R.I.P 面试官们。