此次咱们就不要那么多前戏,直奔主题,咱们的龙门阵正式开始。html
开局一道题,内容全靠吹。(此处应有滑稽)前端
// 文件名: index.js
// 咱们尽可能模拟全部的异步场景,包括 timers、Promise、nextTick等等
setTimeout(() => {
console.log('timeout 1');
}, 1);
process.nextTick(() => {
console.log('nextTick 1');
});
fs.readFile('./index.js', (err, data) => {
if(err) return;
console.log('I/O callback');
process.nextTick(() => {
console.log('nextTick 2');
});
});
setImmediate(() => {
console.log('immediate 1');
process.nextTick(() => {
console.log('nextTick 3');
});
});
setTimeout(() => {
console.log('timeout 2');
process.nextTick(() => {
console.log('nextTick 4');
});
}, 100);
new Promise((resolve, reject) => {
console.log('promise run');
process.nextTick(() => {
console.log('nextTick 5');
});
resolve('promise then');
setImmediate(() => {
console.log('immediate 2');
});
}).then(res => {
console.log(res);
});
复制代码
note: 上面的代码执行环境是 node v10.7.0,浏览器的事件循环和 node 仍是有一点区别的,有兴趣的能够本身找资料看一看。node
好了,上面的代码涉及到定时器、nextTick、Promise、setImmediate 和 I/O 操做。头皮有点小发麻哈,你们想好答案了么?检查一下吧!c++
promise run
nextTick 1
nextTick 5
promise then
timeout 1
immediate 1
immediate 2
nextTick 3
I/O callback
nextTick 2
timeout 2
nextTick 4
复制代码
怎么样?跟本身想的同样么?不同的话,就听我慢慢道来。git
在 Node.js 中,event loop 是基于 libuv 的。经过查看 libuv 的文档能够发现整个 event loop 分为 6 个阶段:github
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
复制代码
event loop 的代码在文件 deps/uv/src/unix/core.c
中。bootstrap
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
// 肯定 event loop 是否继续
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop); // 更新时间
uv__run_timers(loop); // timers 阶段
ran_pending = uv__run_pending(loop); // pending callbacks 阶段
uv__run_idle(loop); // idle 阶段
uv__run_prepare(loop); // prepare 阶段
timeout = 0;
// 设置 poll 阶段的超时时间,有如下状况超时时间设为 0,此时 poll 不会阻塞
// 1. stop_flag 不为 0
// 2. 没有活跃的 handles 和 request
// 3. idle、pending callback、close 阶段 handle 队列不为空
// 不然的话会将超时时间设置成距离当前时间最近的 timer 的时间
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
// poll 阶段
uv__io_poll(loop, timeout);
// check 阶段
uv__run_check(loop);
// close 阶段
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
复制代码
这一小节咱们主要看看 Node 如何将咱们写的定时器等等注册到 event loop 中去并执行的。promise
以 setTimeout 为例,首先咱们进到了 timers.js
这个文件中,找到了 setTimeout
函数,咱们主要关注这么两句:浏览器
function setTimeout(callback, after, arg1, arg2, arg3) {
// ...
const timeout = new Timeout(callback, after, args, false);
active(timeout);
return timeout;
}
复制代码
咱们看到它 new 了一个 Timeout 类,咱们顺着这条线索找到了 Timeout 的构造函数:bash
function Timeout(callback, after, args, isRepeat) {
// ...
this._onTimeout = callback;
// ...
}
复制代码
咱们主要关注这一句,Node 将回调挂载到了 _onTimeout
这个属性上。那么这个回调是在何时执行的呢?咱们全局搜一下 _onTimeout()
,咱们能够发现是一个叫作 ontimeout
的方法执行了回调,好了,咱们开始顺藤摸瓜,能够找到这么一条调用路径 processTimers -> listOnTimeout -> tryOnTimeout -> ontimeout -> _onTimeout
。
最后的最后,咱们在文件的头部发现了这么几行代码:
const {
getLibuvNow,
setupTimers,
scheduleTimer,
toggleTimerRef,
immediateInfo,
toggleImmediateRef
} = internalBinding('timers');
setupTimers(processImmediate, processTimers);
复制代码
咱们一看,setupTimers
是从 internalBinding('timers')
获取的,咱们去看一下 internalBinding
就知道这就是 js 代码和内建模块关联的地方了。因而,咱们顺着这条线索往下找,咱们去 src
目录下去找叫 timers 的文件,果不其然,咱们找到一个叫 timers.cc
的文件,同时,找到了一个叫 SetupTimers
的函数。
void SetupTimers(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsFunction());
CHECK(args[1]->IsFunction());
auto env = Environment::GetCurrent(args);
env->set_immediate_callback_function(args[0].As<Function>());
env->set_timers_callback_function(args[1].As<Function>());
}
复制代码
上面的 args[1]
就是咱们传递的 processTimers
,在这个函数中咱们其实就完成了 processTimers
的注册,它成功的注册到了 node 中。
那是如何触发的回调呢?这里咱们首先先看到 event loop 代码中的 timers 阶段执行的函数,而后跟进去:
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
heap_node = heap_min(timer_heap(loop));
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout > loop->time)
break;
uv_timer_stop(handle);
uv_timer_again(handle);
handle->timer_cb(handle);
}
}
复制代码
这段代码咱们将咱们的目光放在 handle->timer_cb(handle)
这一行,这个函数是在哪儿定义的呢?咱们全局搜一下 timer_cb
发现 uv_timer_start
中有这么一行代码:
handle->timer_cb = cb;
复制代码
因此咱们知道是调用了 uv_timer_start
将回调函数挂载到了 handle 上。那么 cb 又是什么呢?其实你沿着代码上去找就能发现其实 cb 就是 timers_callback_function
,眼熟对么?这就是咱们上面注册进来触发回调的函数 processTimers
。
恍然大悟,原来是这么触发的回调,如今还有个问题,谁去调用的 uv_timer_start
呢?这个问题就简单了,咱们经过源码能够知道是 ScheduleTimer
这个函数调用了,是否是感受很熟悉,对,这个函数就是咱们经过 internalBinding
引进来的 scheduleTimer
函数。
在这个地方就有点不同了。如今最新的 tag 版本和 github 上 node 最新的代码是有区别的,在一次 pr 中,将 timer_wrap.cc
重构成了 timers.cc
,而且移除了 TimerWrap
类,再说下面的区别以前,先补充一下 timer
对应的数据结构:
// 这是在有 TimeWrap 的版本
// 对应的时间后面是一个 timer 链表
refedLists = {
1000: TimeWrap._list(TimersList(item<->item<->item<->item)),
2000: TimeWrap._list(TimersList(item<->item<->item<->item)),
};
// 这是 scheduleTimer 的版本
refedLists = {
1000: TimersList(item<->item<->item<->item),
2000: TimersList(item<->item<->item<->item),
};
复制代码
在 TimeWrap
的版本里,js 是经过调用实例化后的 start()
函数去调用了 uv_timer_start
。
而 scheduleTimer
版本是注册定时器的时候经过比较哪一个定时器是最近要执行的,从而将对应时间的 timerList
注册到 uv_timer
中去。
那么,为何要这么改呢?是为了让定时器和 Immediate 拥有更类似的行为,也就是将单个 uv_timer_t handle 存在 Environment 上(Immediate 是有一个 ImmediateQueue,这也是个链表)。
这里就只说了一个 timer,其余的你们就本身去看看吧,顺着这个思路你们确定会有所收获的。
在加载 node 的时候,将 setTimeout、setInterval 的回调注册到 timerList,将 Promise.resolve 等 microTask 的回调注册到 microTasks,将 setImmediate 注册到 immediateQueue 中,将 process.nextTick 注册到 nextTickQueue 中。
当咱们开始 event loop 的时候,首先进入 timers 阶段(咱们只看跟咱们上面说的相关的阶段),而后就判断 timerList 的时间是否到期了,若是到期了就执行,没有就下一个阶段(其实还有 nextTick,等下再说)。
接下来咱们说 poll 阶段,在这个阶段,咱们先计算须要在这个阶段阻塞轮询的时间(简单点就是下个 timer 的时间),而后等待监听的事件。
下个阶段是 check 阶段,对应的是 immediate,当有 immediateQueue 的时候就会跳过 poll 直接到 check 阶段执行 setImmediate 的回调。
那有同窗就要问了,nextTick 和 microTasks 去哪儿了啊?别慌,听我慢慢道来。
如今咱们有了刚刚找 timer 的经验,咱们继续去看看 nextTick 是怎么执行的。
通过排查咱们能找到一个叫 _tickCallback
的函数,它不断的从 nextTickQueue 中获取 nextTick 的回调执行。
function _tickCallback() {
let tock;
do {
while (tock = queue.shift()) {
// ...
const callback = tock.callback;
if (tock.args === undefined)
callback();
else
Reflect.apply(callback, undefined, tock.args);
emitAfter(asyncId);
}
tickInfo[kHasScheduled] = 0;
runMicrotasks();
} while (!queue.isEmpty() || emitPromiseRejectionWarnings());
tickInfo[kHasPromiseRejections] = 0;
}
复制代码
咱们看到了什么?在将 nextTick 的回调执行完以后,它执行了 runMicrotasks
。一切都真相大白了,microTasks 的执行时机是当执行完全部的 nextTick 的回调以后。那 nextTick 又是在何时执行的呢?
这就须要咱们去找 C++ 的代码了,在 bootstrapper.cc
里找到了 BOOTSTRAP_METHOD(_setupNextTick, SetupNextTick)
,因此咱们就要去找 SetupNextTick
函数。
void SetupNextTick(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
// ...
env->set_tick_callback_function(args[0].As<Function>());
// ...
}
复制代码
咱们关注这一句,是否是很熟啊,跟上面 timer 同样是吧,咱们将 __tickCallback
注册到了 node,在 C++ 中经过 tick_callback_function
来调用这个函数。
咱们经过查看源码能够发现是 InternalCallbackScope
这个类调用 Close
函数的时候就会触发 nextTixk 执行。
void InternalCallbackScope::Close() {
if (closed_) return;
closed_ = true;
HandleScope handle_scope(env_->isolate());
// ...
if (!tick_info->has_scheduled()) {
env_->isolate()->RunMicrotasks();
}
// ...
if (!tick_info->has_scheduled() && !tick_info->has_promise_rejections()) {
return;
}
// ...
if (env_->tick_callback_function()->Call(process, 0, nullptr).IsEmpty()) {
failed_ = true;
}
}
复制代码
可能有同窗有疑问了,为啥在执行 nextTick 上面还有 RunMicrotasks
呢?其实这是对 event loop 的优化,假如没有 process.nextTick
就直接从 node 里面调用 RunMicrotasks
加快速度。
如今在 node.cc
里咱们找到了调用 Close
的地方:
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);
scope.Close();
return ret;
}
复制代码
而 InternalMakeCallback()
则是在 async_wrap.cc
的 AsyncWrap::MakeCallback()
中被调用。
找了半天,只找到了 setImmediate 注册时,注册函数执行回调运行了这个函数,没有找到 timer 的。以前由于使用的 TimeWrap,TimeWrap 继承了 AsyncWrap,在执行回调的时候调用了 MakeCallback()
,问题是如今移除了 TimeWrap
,那是怎么调用的呢?咱们会到 js 代码,发现了这样的代码:
const { _tickCallback: runNextTicks } = process;
function processTimers(now) {
runNextTicks();
}
复制代码
一切都明了了,在移除了 TimeWrap
以后,将 _tickCallback
放到了这里执行,因此咱们刚刚在 C++ 里找不到。
其实,每个阶段执行完以后,都会去执行 _tickCallback
,只是方式可能有点不一样。
好了,刚刚了解了关于 event loop 的一些状况,咱们再来看看文章开头的那段代码,咱们一块儿来分析。
首先运行 Promise 里的代码,输出了 promise run,而后 promise.resolve 将 then 放入 microTasks。
这里要提到的一点是 nextTick 在注册以后,bootstrap 构建结束后运行SetupNextTick
函数,这时候就会清空 nextTickQueue 和 MicroTasks,因此输出 nextTick 一、nextTick 五、promise then。
在 bootstrap 以后便进入了 event loop,第一个阶段 timers,这时 timeout 1 定时器时间到期,执行回调输出 timeout 1,timerList 没有其余定时器了,去清空 nextTickQueue 和 MicroTasks,没有任务,这时继续下阶段,这时候有 immediate,因此跳过 poll,进入 check,执行 immediate 回调,输出 immediate 1 和 immediate 2,并将 nextTick 3 推入 nextTickQueue,阶段完成 immediateQueue 没有须要处理的东西了,就去清空 nextTickQueue 和 MicroTasks 输出 nextTick 3。
在这一轮,文件读取完成,而且 timers 没到期,进入 poll 阶段,超时时间设置为 timeout 2 的时间,执行回调输出 I/O callback,而且向 nextTickQueue 推入 nextTick 2。阻塞过程当中没有其余的 I/O 事件,去清空 nextTickQueue 和 MicroTasks,输出 nextTick 2。
这时候又到了 timers 阶段,执行 timeout 2 的回调,输出 timeout 2,将 nextTick 4 推入 nextTickQueue,这时 timeList 已经没有定时器了,清空 nextTickQueue 和 MicroTasks 输出 nextTick 4。
不知道你们懂了没有,整个过程其实还比较粗糙,在学习过程当中也看了很多的源码分析,可是 node 发展很快,不少分析已通过时了,源码改变了很多,可是对于理清思路仍是颇有做用的。
各位看官若是以为还行、OK、有点用,欢迎来我 GitHub 给个小星星,我会很舒服的,哈哈。
文 / 小烜同窗
完美无缺的秘密是:作技术,你快乐吗?
编 / 荧声
编者按: 做者也在玩掘金,关注他呀~
本文已由做者受权发布,版权属于创宇前端。欢迎注明出处转载本文。本文连接:knownsec-fed.com/2018-09-13-…
想要订阅更多来自知道创宇开发一线的分享,请搜索关注咱们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,咱们会尽量回复。
感谢您的阅读。