promise.then, process.nextTick?让我康康具体发生甚么事了

杰克-逊の黑豹,恰饭了啦 []( ̄▽ ̄)

前言

本文是 好激动,离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个问题,解释下:

  1. 为什么脚本执行之后,没有立即进入事件循环,而是跳入processTicksAndRejections函数?
  2. 为什么先执行process.nextTick注册的回调,然后再执行promise.then回调?
  3. 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(
      &params, 
      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;
}

那么,把以上的东西串接起来,就得到最开始的问题的答案:

  1. 编译node阶段,将 processTicksAndRejections 绑定到env;
  2. test.js脚本执行后,InternalCallbackScope的析构函数会执行;
  3. 析构函数内部会由env->tick_callback_function()拿到processTicksAndRejections函数,并执行;
  4. 最终,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 面试官们。

相关推荐
翔云API几秒前
人证合一接口:智能化身份认证的最佳选择
大数据·开发语言·node.js·ocr·php
jimmy.hua几秒前
C++刷怪笼(5)内存管理
开发语言·数据结构·c++
xiaobai12 34 分钟前
二叉树的遍历【C++】
开发语言·c++·算法
DieSnowK10 分钟前
[项目][WebServer][Makefile & Shell]详细讲解
开发语言·c++·http·makefile·shell·项目·webserver
&白帝&25 分钟前
uniapp中使用picker-view选择时间
前端·uni-app
谢尔登32 分钟前
Babel
前端·react.js·node.js
ling1s32 分钟前
C#基础(13)结构体
前端·c#
卸任38 分钟前
使用高阶组件封装路由拦截逻辑
前端·react.js
lxcw1 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js
dc爱傲雪和技术1 小时前
在 VS Code 中调试 C++ 项目
开发语言·c++