本文首发在github,感兴趣请点击此处javascript
nodejs是单线程执行的,同时它又是基于事件驱动的非阻塞IO编程模型。这就使得咱们不用等待异步操做结果返回,就能够继续往下执行代码。当异步事件触发以后,就会通知主线程,主线程执行相应事件的回调。java
以上是众所周知的内容。今天咱们从源码入手,分析一下nodejs的事件循环机制。node
首先,咱们先看下nodejs架构,下图所示:linux
用户代码即咱们编写的应用程序代码、npm包、nodejs内置的js模块等,咱们平常工做中的大部分时间都是编写这个层面的代码。git
胶水代码,可以让js调用C/C++的代码。能够将其理解为一个桥,桥这头是js,桥那头是C/C++,经过这个桥可让js调用C/C++。
在nodejs里,胶水代码的主要做用是把nodejs底层实现的C/C++库暴露给js环境。
三方插件是咱们本身实现的C/C++库,同时须要咱们本身实现胶水代码,将js和C/C++进行桥接。github
nodejs的依赖库,包括大名鼎鼎的V八、libuv。
V8: 咱们都知道,是google开发的一套高效javascript运行时,nodejs可以高效执行 js 代码的很大缘由主要在它。
libuv:是用C语言实现的一套异步功能库,nodejs高效的异步编程模型很大程度上归功于libuv的实现,而libuv则是咱们今天重点要分析的。
还有一些其余的依赖库
http-parser:负责解析http响应
openssl:加解密
c-ares:dns解析
npm:nodejs包管理器
...npm
关于nodejs再也不过多介绍,你们能够自行查阅学习,接下来咱们重点要分析的就是libuv。编程
咱们知道,nodejs实现异步机制的核心即是libuv,libuv承担着nodejs与文件、网络等异步任务的沟通桥梁,下面这张图让咱们对libuv有个大概的印象: windows
这是libuv官网的一张图,很明显,nodejs的网络I/O、文件I/O、DNS操做、还有一些用户代码都是在 libuv 工做的。 既然谈到了异步,那么咱们首先概括下nodejs里的异步事件:promise
对于网络I/O,各个平台的实现机制不同,linux 是 epoll 模型,类 unix 是 kquene 、windows 下是高效的 IOCP 完成端口、SunOs 是 event ports,libuv 对这几种网络I/O模型进行了封装。
libuv内部还维护着一个默认4个线程的线程池,这些线程负责执行文件I/O操做、DNS操做、用户异步代码。当 js 层传递给 libuv 一个操做任务时,libuv 会把这个任务加到队列中。以后分两种状况:
固然,若是以为4个线程不够用,能够在nodejs启动时,设置环境变量UV_THREADPOOL_SIZE来调整,出于系统性能考虑,libuv 规定可设置线程数不能超过128个。
先简要介绍下nodejs的启动过程:
以上就是 nodejs 执行一个js文件的全过程。接下来着重介绍第八个步骤,事件循环。
咱们看几处关键源码:
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
//判断事件循环是否存活。
r = uv__loop_alive(loop);
//若是没有存活,更新时间戳
if (!r)
uv__update_time(loop);
//若是事件循环存活,而且事件循环没有中止。
while (r != 0 && loop->stop_flag == 0) {
//更新当前时间戳
uv__update_time(loop);
//执行 timers 队列
uv__run_timers(loop);
//执行因为上个循环未执行完,并被延迟到这个循环的I/O 回调。
ran_pending = uv__run_pending(loop);
//内部调用,用户不care,忽略
uv__run_idle(loop);
//内部调用,用户不care,忽略
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
//计算距离下一个timer到来的时间差。
timeout = uv_backend_timeout(loop);
//进入 轮询 阶段,该阶段轮询I/O事件,有则执行,无则阻塞,直到超出timeout的时间。
uv__io_poll(loop, timeout);
//进入check阶段,主要执行 setImmediate 回调。
uv__run_check(loop);
//进行close阶段,主要执行 **关闭** 事件
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
//更新当前时间戳
uv__update_time(loop);
//再次执行timers回调。
uv__run_timers(loop);
}
//判断当前事件循环是否存活。
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids * dirtying a cache line. */
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
复制代码
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
//取出定时器堆中超时时间最近的定时器句柄
heap_node = heap_min((struct heap*) &loop->timer_heap);
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);
// 判判定时器句柄类型是不是repeat类型,若是是,从新建立一个定时器句柄。
uv_timer_again(handle);
//执行定时器句柄绑定的回调函数
handle->timer_cb(handle);
}
}
复制代码
void uv__io_poll(uv_loop_t* loop, int timeout) {
/*一连串的变量初始化*/
//判断是否有事件发生
if (loop->nfds == 0) {
//判断观察者队列是否为空,若是为空,则返回
assert(QUEUE_EMPTY(&loop->watcher_queue));
return;
}
nevents = 0;
// 观察者队列不为空
while (!QUEUE_EMPTY(&loop->watcher_queue)) {
/* 取出队列头的观察者对象 取出观察者对象感兴趣的事件并监听。 */
....省略一些代码
w->events = w->pevents;
}
assert(timeout >= -1);
//若是有超时时间,将当前时间赋给base变量
base = loop->time;
// 本轮执行监听事件的最大数量
count = 48; /* Benchmarks suggest this gives the best throughput. */
//进入监听循环
for (;; nevents = 0) {
// 有超时时间的话,初始化spec
if (timeout != -1) {
spec.tv_sec = timeout / 1000;
spec.tv_nsec = (timeout % 1000) * 1000000;
}
if (pset != NULL)
pthread_sigmask(SIG_BLOCK, pset, NULL);
// 监听内核事件,当有事件到来时,即返回事件的数量。
// timeout 为监听的超时时间,超时时间一到即返回。
// 咱们知道,timeout是传进来得下一个timers到来的时间差,因此,在timeout时间内,event-loop会一直阻塞在此处,直到超时时间到来或者有内核事件触发。
nfds = kevent(loop->backend_fd,
events,
nevents,
events,
ARRAY_SIZE(events),
timeout == -1 ? NULL : &spec);
if (pset != NULL)
pthread_sigmask(SIG_UNBLOCK, pset, NULL);
/* Update loop->time unconditionally. It's tempting to skip the update when * timeout == 0 (i.e. non-blocking poll) but there is no guarantee that the * operating system didn't reschedule our process while in the syscall. */
SAVE_ERRNO(uv__update_time(loop));
//若是内核没有监听到可用事件,且本次监听有超时时间,则返回。
if (nfds == 0) {
assert(timeout != -1);
return;
}
if (nfds == -1) {
if (errno != EINTR)
abort();
if (timeout == 0)
return;
if (timeout == -1)
continue;
/* Interrupted by a signal. Update timeout and poll again. */
goto update_timeout;
}
。。。
//判断事件循环的观察者队列是否为空
assert(loop->watchers != NULL);
loop->watchers[loop->nwatchers] = (void*) events;
loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
// 循环处理内核返回的事件,执行事件绑定的回调函数
for (i = 0; i < nfds; i++) {
。。。。
}
}
复制代码
uv__io_poll阶段源码最长,逻辑最为复杂,能够作个归纳,以下: 当js层代码注册的事件回调都没有返回的时候,事件循环会阻塞在poll阶段。看到这里,你可能会想了,会永远阻塞在此处吗?
一、首先呢,在poll阶段执行的时候,会传入一个timeout超时时间,该超时时间就是poll阶段的最大阻塞时间。
二、其次呢,在poll阶段,timeout时间未到的时候,若是有事件返回,就执行该事件注册的回调函数。timeout超时时间到了,则退出poll阶段,执行下一个阶段。
因此,咱们不用担忧事件循环会永远阻塞在poll阶段。
以上就是事件循环的两个核心阶段。限于篇幅,timers阶段的其余源码和setImmediate、process.nextTick的涉及到的源码就不罗列了,感兴趣的童鞋能够看下源码。
最后,总结出事件循环的原理以下,以上你能够不care,记住下面的总结就行了。
细心的童鞋能够发现,在事件循环的每个子阶段退出以前都会按顺序执行以下过程:
记住这个规律哦。
那么,按照以上公式,代入网上各类有关 nodejs 事件循环的测试代码,相信你已经可以解释为何会输出那样的结果了。若是不能,那就私信我吧~~