这篇文章主要介绍nextTick和RunMicrotasks的主要流程和涉及到的相关源码,对于timers相关api在event-loop中的表现不做解读
nextTick实现
目光直接转移到next_tick.js,整体nextTick的代码其实很容易理解:
const [
tickInfo,
runMicrotasks
] = process._setupNextTick(_tickCallback);
function nextTick(callback) {
// ...
nextTickQueue.push(new TickObject(callback, args, getDefaultTriggerAsyncId()));
}
function _tickCallback() {
let tock;
do {
while (tock = nextTickQueue.shift()) {
// ...
const callback = tock.callback;
if (tock.args === undefined)
callback();
runMicrotasks();
} while (nextTickQueue.head !== null || emitPromiseRejectionWarnings());
tickInfo[kHasPromiseRejections] = 0;
}
通过这两个函数,就能看出来整个nextTick是如何工作的。
- nextTickQueue为记录nextTick的数组,有新的nextTick注册进来就会被推入数组
- _tickCallback则会不断的推出数组中的元素然后运行
大家注意一下process._setupNextTick(_tickCallback)
,最终这个_tickCallback并没有在js中执行,而是传递给了c++:
// node.cc
void SetupNextTick(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsFunction());
env->set_tick_callback_function(args[0].As<Function>());
// ...
在这里可以看出来,最终_tickCallback丢给了tick_callback_function
,然后在LoadEnvironment
中通过_setupNextTick
触发运行(LoadEnvironment
之前详细介绍过,在这里不做过多介绍),在这里简单的追踪了一下_tickCallback来证实一下最终_tickCallback传递给了tick_callback_function
:
process.nextTick(()=>console.log(2))
tips: 蓝色底色代码为断点所在位置,下方为此时刻的内存地址,上面这张图可以看出来在没有跑LoadEnvironment
的时候,tick_callback_function
为NULL
如果对LoadEnvironment
比较了解的读者,应该是明白其中的原理的,如果不明白原理可以简单看一下tick_callback_function
这里的内存变化。这里我们假设读者了解node启动的所有机制,那么就会发现一件事情:在process.nextTick
运行的时候,uv_run
尚未启动。
那么,我们可以根据这个显现得出一个比较浅显的结论:process.nextTick
会阻塞libuv的事件循环。(这是在node初始化bootatrap阶段的情况。即使在evnt_loop中,表现也是一样的。为何用这个阶段来叙述,是因为这个阶段最容易追踪和解读)
process.nextTick和RunMicrotasks
通过前一章节的叙述和上一篇文章对setTimeout流程的分析,我们可以发现:process.nextTick
不是基于libuv事件机制的,而timers一系列的api全部是基于libuv开放出来的api实现的。那么这个nextTick到底是如何实现的呢?
接下来就要从nextTick的源码聊起了:
function _tickCallback() {
let tock;
do {
while (tock = nextTickQueue.shift()) {
// ...
const callback = tock.callback;
if (tock.args === undefined)
callback();
// ...
}
runMicrotasks();
}
// ...
}
在执行完nextTick之后(callback()
)还继续执行了runMicrotasks
,我相信如果了解过Microtasks的读者肯定知道这到底是做什么的,接下来我们深扒一下这个runMicrotasks
:
// src/node.cc
v8::Local<v8::Function> run_microtasks_fn =
env->NewFunctionTemplate(RunMicrotasks)->GetFunction(env->context())
.ToLocalChecked();//v8 吐出来的方法 RunMicrotasks
run_microtasks_fn->SetName(
FIXED_ONE_BYTE_STRING(env->isolate(), "runMicrotasks"));
// deps/v8/src/isolate.cc
void Isolate::RunMicrotasks() {// v8中RunMicrotasks实现
// Increase call depth to prevent recursive callbacks.
v8::Isolate::SuppressMicrotaskExecutionScope suppress(
reinterpret_cast<v8::Isolate*>(this));
is_running_microtasks_ = true;
RunMicrotasksInternal();
is_running_microtasks_ = false;
FireMicrotasksCompletedCallback();
}
void Isolate::RunMicrotasksInternal() {
if (!pending_microtask_count()) return;
TRACE_EVENT0("v8.execute", "RunMicrotasks");
TRACE_EVENT_CALL_STATS_SCOPED(this, "v8", "V8.RunMicrotasks");
while (pending_microtask_count() > 0) {
HandleScope scope(this);
int num_tasks = pending_microtask_count();
Handle<FixedArray> queue(heap()->microtask_queue(), this);
DCHECK(num_tasks <= queue->length());
set_pending_microtask_count(0);
heap()->set_microtask_queue(heap()->empty_fixed_array());
// ...
通过上面的代码,可以比较清晰地看到整个RunMicrotasks
的全过程,主要就是通过microtask_queue来实现的Microtask。
了解了整个流程,可以很容易得出一个结论:nextTick会在v8执行Microtasks之前对在js中注册的nextTickQueue逐个执行,即阻塞了Microtasks执行。
bootstrap阶段和event-loop时候的异同
通过上面的分析,下面这段代码在bootstrap阶段,应该很容易理解:
setTimeout(()=>console.log('timers API'),0)//uv_run开始运行后才执行timers相关api,最后执行
console.log('bootstrap')//在node LoadEnvironment(bootstrap)阶段执行,最先执行
new Promise((resolve,reject)=> resolve('microtask run')).then(arg => console.log(arg))//注册到microtask_queue中
process.nextTick(()=>console.log('run next tick'))// 会在microtask之前运行
结果如图: 相关解释已经写到了上面的注释中。 (当然这里用console来作为同步代码不是很严谨,不过比较直观)
那么在event-loop中是如何表现的呢?在上文中也提到过一句:
这是在node初始化,即bootstrap的情况下,即使在evnt_loop中,表现也是一样的
event-loop中的区别是:本应该在node LoadEnvironment(bootstrap)阶段执行的代码的运行转移到了InternalMakeCallback
中。
下面是InternalMakeCallback
的代码:
// ./src/node.cc
MaybeLocal<Value> InternalMakeCallback(Environment* env,
Local<Object> recv,
const Local<Function> callback,
int argc,
Local<Value> argv[],
async_context asyncContext) {
CHECK(!recv.IsEmpty());
InternalCallbackScope scope(env, recv, asyncContext);
if (scope.Failed()) {
return Undefined(env->isolate());
}
MaybeLocal<Value> ret;
{
ret = callback->Call(env->context(), recv, argc, argv);
// ...
}
// ...
return ret;
}
通过ret = callback->Call(env->context(), recv, argc, argv);
实现了event-loop中主体代码的运行,之后在InternalMakeCallback结束之后,实现对nextTick和microtask的调用,代码如下:
// ./src/node.cc
void InternalCallbackScope::Close() {
// ...
Environment::TickInfo* tick_info = env_->tick_info();
if (!tick_info->has_scheduled()) {
env_->isolate()->RunMicrotasks();
}
// ...
if (!tick_info->has_scheduled() && !tick_info->has_promise_rejections()) {
return;
}
// ...
Local<Object> process = env_->process_object();
if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
failed_ = true;
}
}
其中,有两个需要注意的地方,一个是:
if (!tick_info->has_scheduled()) {
env_->isolate()->RunMicrotasks();
}
// ...
if (!tick_info->has_scheduled() && !tick_info->has_promise_rejections()) {
return;
}
这两处代码专门针对无process.nextTick行为的event-loop进行了处理,直接从node中调用v8的RunMicrotasks,加快整体处理速度。
另外一个地方是:
if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
failed_ = true;
}
通过对tick_callback_function的调用,实现触发之前讲过的_tickCallback
,不知道大家还记得这句话么:
在这里简单的追踪了一下_tickCallback来证实一下最终_tickCallback传递给了
tick_callback_function
这样,整体形成了一个闭环,无论是bootstrap阶段还是在event-loop阶段,总是能保证两点:
- nextTick永远在主函数(包括同步代码和console)运行完之后运行
- nextTick永远优先于microtask运行
by 小菜 原文地址:https://github.com/xtx1130/blog/issues/16,欢迎star和watch,如果文中有讲解错误的地方欢迎指正