eth 以太坊系列线下活动:《开发者的以太坊进阶指南》北京站来了!开发者的以太坊进阶指南 | Jeth 以太坊系列线下活动第四期北京场
process 是一个全局对象,它提供了当前 Node.js 线程的相关信息和一些控制方法。因为 process 挂载了太多属性和方法,这篇文章先从 process.nextTick() 开始吧。
function setupNextTick() {
// 设置 Promise 模块的调度方法
const promises = require('internal/process/promises');
const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks);
var nextTickQueue = [];
// microtask 标记
var microtasksScheduled = false;
// 接收 V8 micro task 队列的运行的对象.
var _runMicrotasks = {};
// 这里 kIndex kLength 是一个约定的 Environment::TickInfo 的 index 和 length 的索引
var kIndex = 0;
var kLength = 1;
process.nextTick = nextTick;
// Needs to be accessible from beyond this scope.
process._tickCallback = _tickCallback;
process._tickDomainCallback = _tickDomainCallback;
// 通过 process._setupNextTick 注册 _tickCallback, 获取 _runMicrotasks
// `tickInfo` 也接收了 `process._setupNextTick()` 的返回参数,通过 `tickInfo` 能使 C++ 模块能访问到 nextTick 队列的状态。
const tickInfo = process._setupNextTick(_tickCallback, _runMicrotasks);
// 接收驱动 V8's micro task 队列的方法
_runMicrotasks = _runMicrotasks.runMicrotasks;
function tickDone() {
...
}
function scheduleMicrotasks() {
...
}
function runMicrotasksCallback() {
...
}
function _combinedTickCallback() {
...
}
function _tickCallback() {
...
}
function _tickDomainCallback() {
...
}
function nextTick() {
...
}
}
这里两个大体都是执行一定数量( 最大 1e4 )的数量 callbacks, 前者不需要执行 domain 进入上下文。
function _tickCallback() {
var callback, args, tock;
do {
while (tickInfo[kIndex] < tickInfo[kLength]) {
tock = nextTickQueue[tickInfo[kIndex]++];
callback = tock.callback;
args = tock.args;
_combinedTickCallback(args, callback);
if (kMaxCallbacksPerLoop < tickInfo[kIndex])
tickDone();
}
tickDone();
// V8 promise microtasks
_runMicrotasks();
emitPendingUnhandledRejections();
} while (tickInfo[kLength] !== 0);
}
这里的参数处理还是体现了 Nodejs 中贯穿的性能追求以及 80/20 的理念。
function _combinedTickCallback(args, callback) {
if (args === undefined) {
callback();
} else {
switch (args.length) {
case 1:
callback(args[0]);
break;
case 2:
callback(args[0], args[1]);
break;
case 3:
callback(args[0], args[1], args[2]);
break;
default:
callback.apply(null, args);
}
}
}
执行正常的清理操作,删除刚执行完的 callback 或者 清空队列。
function tickDone() {
if (tickInfo[kLength] !== 0) {
if (tickInfo[kLength] <= tickInfo[kIndex]) {
nextTickQueue = [];
tickInfo[kLength] = 0;
} else {
// 推出队列的首个元素
nextTickQueue.splice(0, tickInfo[kIndex]);
tickInfo[kLength] = nextTickQueue.length;
}
}
tickInfo[kIndex] = 0;
}
再回头看一下 setupNextTick 中的 Promise setup 那段 promises.setup(scheduleMicrotasks)。下面我们来看看 scheduleMicrotasks.
function setupNextTick() {
const promises = require('internal/process/promises');
const emitPendingUnhandledRejections = promises.setup(scheduleMicrotasks);
var microtasksScheduled = false;
...
}
先判断 microtasksScheduled ,如果为 false 就会执行到给 nextTickQueue 添加一个新的节点,callback 为 runMicrotasksCallback。接着 tickInfo[kLength] 增加 1 并将 microtasksScheduled 设置为 true , 确保在未执行 microtask 之前不会重复执行。
function scheduleMicrotasks() {
if (microtasksScheduled)
return;
nextTickQueue.push({
callback: runMicrotasksCallback,
domain: null
});
tickInfo[kLength]++;
microtasksScheduled = true;
}
可以看到这里与之前对应的, 这里首先执行 microtasksScheduled = false, 接着调用 _runMicrotasks。在 nextTickQueue 以及 Promise 还有 Listeners 时继续调用 scheduleMicrotasks 来向 nextTickQueue 添加 callback。
function runMicrotasksCallback() {
microtasksScheduled = false;
_runMicrotasks();
if (tickInfo[kIndex] < tickInfo[kLength] ||
emitPendingUnhandledRejections())
scheduleMicrotasks();
}
通过上面的 JS 部分我们了解到,process.nextTick, Microtasks 以及 Promise 的 callback 都是通过一个队列 nextTickQueue 调度, 而这一切都是从
_tickCallback ( _tickDomainCallback )开始的。
// src/node.cc
void SetupNextTick(const FunctionCallbackInfo& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsFunction());
CHECK(args[1]->IsObject());
// 将之前的 `_tickCallback` 设置到环境变量中 tick_callback_function
env->set_tick_callback_function(args[0].As());
// 将传过来的 _runMicrotasks ({}) 对象添加 runMicrotasks 方法
env->SetMethod(args[1].As
RunMicrotasks() 是 v8 暴露的一个 API 方法 https://cs.chromium.org/chromium/src/v8/src/api.cc?q=RunMicrotasks&dr=CSs&l=8512
上面设置 tick_callback_function,那么这个 process.nextTick() 是什么时候被调用?
// src/async-wrap.cc
Local AsyncWrap::MakeCallback(const Local cb,
int argc,
Local* argv) {
...
Local ret = cb->Call(context, argc, argv);
...
Environment::TickInfo* tick_info = env()->tick_info();
// 如果 nextTick 队列为空时执行 RunMicrotasks
if (tick_info->length() == 0) {
env()->isolate()->RunMicrotasks();
}
Local
// - MakeCallback may only be made directly off the event loop.
// That is there can be no JavaScript stack frames underneath it.
// MakeCallback 会直接在 event loop 中执行。
class HandleWrap : public AsyncWrap {
public:
...
uv_handle_t* const handle_;
};
比如下面的 UDPWrap 是 Nodejs 的 udp 协议 (User Datagram Protocol)的封装层,它继承自 HandleWrap
// src/udp_wrap.cc
class UDPWrap: public HandleWrap {
public:
...
private:
...
uv_udp_t handle_;
};
AsyncWrap 是 Nodejs 中大多数 IO 封装层都是基于 HandleWrap。HandleWrap 继承自 AsyncWrap , 所以 process.nextTick 和 microtask 基本是在 uv__io_poll 阶段调用, 为什么说是主要,因为有两个其他情况,继续往下看。
node 初始化运行的时候会调用 process._tickCallback()
// lib/module.js
// bootstrap main module.
Module.runMain = function() {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
};
如果应用中抛出异常,未被捕获的话退出线程,有捕获的话,应用不会崩溃退出,而是调用 setImmediate 执行 process._tickCallback(), 也就是说 process.nextTick 也可能在 Check 阶段被调用。
function setupProcessFatal() {
process._fatalException = function(er) {
var caught;
if (process.domain && process.domain._errorHandler)
caught = process.domain._errorHandler(er) || caught;
if (!caught)
caught = process.emit('uncaughtException', er);
// 如果没有函数处理这个异常,C++ 结束
if (!caught) {
try {
if (!process._exiting) {
process._exiting = true;
process.emit('exit', 1);
}
} catch (er) {
// nothing to be done about it at this point.
}
} else {
// 如果捕获了这个异常,在 `setImmediate` 中调用 `_tickCallback()` 继续处理 nextTick 队列
NativeModule.require('timers').setImmediate(process._tickCallback);
}
return caught;
};
}
scheduleMicrotasks?
function scheduleMicrotasks() {
if (microtasksScheduled)
return;
nextTickQueue.push({
callback: runMicrotasksCallback,
domain: null
});
tickInfo[kLength]++;
microtasksScheduled = true;
}
// src/node.cc
Local MakeCallback() {
...
// V8 RunMicrotasks
if (tick_info->length() == 0) {
env->isolate()->RunMicrotasks();
}
...
return ret;
}
V8 中 microtask 默认是自动运行的。因为 Promise 处理的异步场景和绝大多数 Nodejs 中异步IO 是紧密相关的,所以在 Nodejs 中默认关闭了自动运行而通过 Nodejs 自行触发 RunMicrotasks()。结合上面的代码也可以基本得出结论 Nodejs 中 Promise 和 process.nextTick() 回调的执行阶段是比较相似的。
inline int Start(..) {
...
isolate->SetAutorunMicrotasks(false);
...
process.nextTick() 一般是在 poll 阶段被执行,也有可能在 check 阶段执行。Promise所处的 Microtasks 是通过调用 V8 暴露的 RunMicrotasks() 方法执行,RunMicrotasks() 会在 process.nextTick() 队列执行,也会在 node::MakeCallback 中执行。