blog
blog copied to clipboard
node源码粗读(7):nextTick和microtasks从bootstrap到event-loop全阶段解读
这篇文章主要介绍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 小菜
有个地方想请教一下,node 的官方文档写明event loop分为timer、io callbacks 、poll几个phrase,在code.c的uv_run
函数中,确实是依次执行了这几个phrase。文档中还指出process.nextTick不属于上面任何一个phrase,而是在每个phrase的执行之后都会执行,这相当于uv_run每执行完一个phrase就被阻塞了,感觉精确阻塞一个函数好像是不可能的,不知道这在源码层面是如何实现,请指教
@youth7 这篇文章提过一句:
通过ret = callback->Call(env->context(), recv, argc, argv);实现了event-loop中主体代码的运行,之后在InternalMakeCallback结束之后,实现对nextTick和microtask的调用
通过这篇文章和上一篇文章能关联出来:上篇文章中说过OnTimeout
函数,这个函数最终调用了MakeCallback
,而MakeCallback
最终调用了InternalMakeCallback
,InternalMakeCallback
之后会调用InternalCallbackScope::Close()
。
@xtx1130
没错,关键之处是InternalMakeCallback
会调用InternalCallbackScope::Close()
,InternalCallbackScope::Close()
里面的env_->tick_callback_function()
最终实现了nextTick的callback的运行。不过我还有的疑问是,如果上面说的是正确的,那么InternalMakeCallback
每次都会调用Close
,这意味着每个phrase的每个callback运行之后都会触发nextTick的callback,这貌似跟
每个phrase的callback执行完毕之后才触发nextTick的callback
有点不符合,不知道我是错过了什么地方
@youth7
每个phrase的每个callback运行之后都会触发nextTick的callback
(下面的表述是基于setTimeout
或者setInterval
中)
一个phrase是通过InternalMakeCallback
运行,运行完之后调用InternalCallbackScope::Close()
。我不太明白你这句话要表达什么意思。InternalMakeCallback
运行的就是他们注册的函数,通过上一篇文章你也能看出来,最终timers api的执行逻辑都是在js中,而node是作为v8和libuv的桥梁的存在,而一个phrase中运行的是此时间点注册的所有函数,在运行完这个时间点所有注册的函数之后,会执行nextTick
中注册的函数。你想表达的意思是运行之后
和执行完毕之后
这个措辞的问题吗?
你好,麻烦问下
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之前运行
这段逻辑为什么run next tick和microtask run会在uv_run之前执行呢?Node官网也有说明,在event loop前会清空next_tick队列,但是在源码上没有找到
@tsy77 在bootstrap阶段触发就是通过process._tickCallback();
触发的,你可以查看一下./lib/internal/bootstrap/node.js的代码。只有在event loop中才会在c++中通过InternalCallbackScope::Close
触发
@xtx1130 了解了 感谢!