node源码粗读(7):nextTick和microtasks从bootstrap到event-loop全阶段解读
发布于 1 个月前 作者 xtx1130 299 次浏览 来自 分享

这篇文章主要介绍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))

issue16-1

tips: 蓝色底色代码为断点所在位置,下方为此时刻的内存地址,上面这张图可以看出来在没有跑LoadEnvironment的时候,tick_callback_function为NULL

issue16-2 如果对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之前运行

结果如图: issue16-3 相关解释已经写到了上面的注释中。 (当然这里用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,如果文中有讲解错误的地方欢迎指正

回到顶部