此处如无特殊指出的话,event loop的语境都是指nodejsjavascript
本文研究所用的nodejs环境是:操做系统window10 + nodejs版本号为v12.16.2html
event loop是指由libuv提供的,一种实现非阻塞I/O的机制。具体来说,由于javascript一门single-threaded编程语言,因此nodejs只能把异步I/O操做的实现(非阻塞I/O的实现结果的就是异步I/O)转交给libuv来作。由于I/O既可能发生在不少不一样操做系统上(Unix,Linux,Mac OX,Window),又能够分为不少不一样类型的I/O(file I/O, Network I/O, DNS I/O,database I/O等)。因此,对于libuv而言,若是当前系统对某种类型的I/O操做提供相应的异步接口的话,那么libuv就使用这些现成的接口,不然的话就启动一个线程池来本身实现。这就是官方文档所说的:“事件循环使Node.js能够经过将操做转移到系统内核中来执行非阻塞I / O操做(尽管JavaScript是单线程的)”的意思。java
The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.node
在继续讨论nodejs event loop以前,咱们不妨来看看nodejs的架构图:linux
从上面的架构图,你能够看出,libuv是位于架构的最底层的。而咱们所要讲得event loop的实现是由libuv来提供的。如今,你的脑海里面应该有一幅完整的画面,并清楚地知道event loop到底处在哪一个位置了。chrome
这里值得强调的一点是,不管是chrome浏览器中的仍是nodejs中的event loop,其实都不是由v8引擎来实现的。数据库
常常听到这样的说法,用户的javascript代码跑在主线程上,nodejs其他的javascript代码(不是用户写的)跑在event loop的这个线程上。每一次当有异步操做发生的时候,主线程会把I/O操做的实现交给event loop线程。当异步I/O有告终果以后,event loop线程就会把结果通知主线程,主线程就会去执行用户注册的callback函数。编程
无论是用户写的仍是nodejs自己内置的javascript代码(nodejs API),全部的javascript代码都运行在同一个线程里面。在nodejs的角度看来,全部的javascript代码要么是同步代码,要么就是异步代码。或许咱们能够这样说,全部的同步代码的执行都是由v8来完成的,全部异步代码的执行都是由libuv提供的event loop功能模块来完成的。那event loop与v8是什么关系呢?咱们能够看看下面的源代码:promise
Environment* CreateEnvironment(Isolate* isolate, uv_loop_t* loop, Handle<Context> context, int argc, const char* const* argv, int exec_argc, const char* const* exec_argv) {
HandleScope handle_scope(isolate);
Context::Scope context_scope(context);
Environment* env = Environment::New(context, loop);
isolate->SetAutorunMicrotasks(false);
uv_check_init(env->event_loop(), env->immediate_check_handle());
uv_unref(reinterpret_cast<uv_handle_t*>(env->immediate_check_handle()));
uv_idle_init(env->event_loop(), env->immediate_idle_handle());
uv_prepare_init(env->event_loop(), env->idle_prepare_handle());
uv_check_init(env->event_loop(), env->idle_check_handle());
uv_unref(reinterpret_cast<uv_handle_t*>(env->idle_prepare_handle()));
uv_unref(reinterpret_cast<uv_handle_t*>(env->idle_check_handle()));
// Register handle cleanups
env->RegisterHandleCleanup(reinterpret_cast<uv_handle_t*>(env->immediate_check_handle()), HandleCleanup, nullptr);
env->RegisterHandleCleanup(reinterpret_cast<uv_handle_t*>(env->immediate_idle_handle()), HandleCleanup, nullptr);
env->RegisterHandleCleanup(reinterpret_cast<uv_handle_t*>(env->idle_prepare_handle()), HandleCleanup, nullptr);
env->RegisterHandleCleanup(reinterpret_cast<uv_handle_t*>(env->idle_check_handle()), HandleCleanup, nullptr);
if (v8_is_profiling) {
StartProfilerIdleNotifier(env);
}
Local<FunctionTemplate> process_template = FunctionTemplate::New(isolate);
process_template->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "process"));
Local<Object> process_object = process_template->GetFunction()->NewInstance();
env->set_process_object(process_object);
SetupProcessObject(env, argc, argv, exec_argc, exec_argv);
LoadAsyncWrapperInfo(env);
return env;
}
复制代码
能够看到,nodejs在建立v8环境的时候,会把libuv默认的event loop做为参数传递进去的。event loop是被v8所使用一个功能模块。所以,咱们能够说,v8包含了event loop。浏览器
对于这个单一的线程,有些人称之为v8线程,有些人称之为event loop线程,还有些人称之为node线程。鉴于nodejs大多时候都被称为javascript的运行时,因此,我更倾向于称之为“node线程”。不过,须要重申一次的是:“不管它叫什么,本质都是同样的。那就是它们都是指全部javascript运行所在的那一个线程。”
异步操做,好比像文件系统的读写,发出HTTP请求或者对数据库进行读写等等都是load off给libuv的线程池来完成的。
libuv确实会建立一个具备四个线程的线程池。可是,时至今日,许多操做系统已经向外提供一些实现异步I/O的接口了(例如:Linux上面的AIO),libuv内部会优先考虑使用这些现成的API接口来完成异步I/O。只有在特定状况下(某个操做系统对某种类型I/O没有提供相应的异步接口的时候),libuv才会使用线程池中的线程+轮询来实现异步I/O。
event loop会持续地以一种FIFO的方式遍历一个装满着异步task callback的队列,当这个task完成以后,event loop就会执行它相应的callback。
event loop机制中确实是涉及到相似于队列的数据结构,可是并非只有一个这种“队列”。实际上,event loop主要遍历的是不一样的阶段(phase),每一个阶段会有一个装着callback函数的队列与之相对应(称之为callback queue)。当执行到某个阶段的时候,event loop才会去遍历这个阶段所对应的callback queue。
首先,咱们从nodejs程序生命周期的角度来看看,event loop所处的位置:
上面的图中,mainline code指的就是咱们nodejs的入口文件。入口文件被看做是同步代码,由v8来执行。在从上到下的解释/编译的过程当中,若是遇到执行异步代码的请求的时候,nodejs就会把它交给event loop来执行。
在nodejs中,异步代码有不少类型,好比定时器,process.nextTick()和各类的I/O操做。上面的这张图把异步I/O单独拎出来,主要是由于在nodejs中,它占据异步代码的大半壁江山,处于十分重要的地位。这里的“Event Demultiplexer”其实指的就是由libuv中帮咱们封装好的各个I/O功能模块的集合(能够查看上面的libuv架构图)。当Event Demultiplexer从操做系统中拿到I/O处理结果后,它就会通知event loop将相应的callback/handler入队到相应的队列中。
event loop是一个单线程,半无限的循环。之因此说它是“半无限”,是由于当没有任何任务(更多的异步I/O请求或者timer)要作的的时候,event loop会退出这个循环,整个nodejs程序也就执行完成了。
以上是event loop在整个nodejs程序生命周期里面的位置。当咱们单独对event loop展开来看的的时候,实际上它主要是包括六个阶段:
event loop会依次进入上述的每一个阶段。每一个阶段都会有一callback queue与之相对应。event loop会遍历这个callback queue,执行里面的每个callback。直到callback queue为空或者当前callback的执行数量超过了某个阈值为止,event loop才会移步到下一个阶段。
在这个阶段,event loop会检查是否有到期的定时器能够执行。若是有,则执行。调用setTimeout或者setInterval方法时传入的callback会在指定的延迟时间后入队到timers callback queue 。跟浏览器环境中的setTimeout和setInterval方法同样,调用时候传入的延迟时间并非回调确切执行的时间。timer callback的执行时间点没法获得稳定的,一致的保证,由于它们的执行会受到操做系统调度层面和其余callback函数调用耗时的影响。因此,对传入setTimeout或者setInterval方法的延迟时间参数正确的指望是:在我指定的延迟时间后,nodejs啊,我但愿你尽快地帮我执行个人callback。也就是说timer callback函数的执行只会比咱们预约的时间的要晚,不会比咱们预约的时间要早。
从技术上来讲,poll阶段实际控制了timer callback执行的时间点。
这个阶段主要是执行某些系统层级操做的回调函数。好比说,TCP发生错误时候的错误回调。假如一个TCP socket在尝试创建链接的时候发生了“ECONNREFUSED”错误,则nodejs须要将对应的错误回调入队到pending callback queue中,并立刻执行,以此来通知操做系统。
只供nodejs内部来用的阶段。对于开发者而言,几乎能够忽略。
在进入轮询阶段以前,event loop会检查timer callback queue是否为空,若是不为空的话,那么event loop就会回退到timer阶段,依次执行全部的timer callback才回到轮询阶段。
进入轮询阶段后,event loop会作两件事:
由于nodejs是志在应用于I/O密集型软件,因此,在一个event loop循环中,它会花费很大比例的时间在轮询阶段。在这个阶段,event loop要么处于执行I/O callback状态,要么处于轮询等待的状态。固然,轮询阶段占用event loop的时间也会是有个限度的。这就是第一件事情要完成的事-计算出有一个切合当前操做系统环境的适合的最大时间值。event loop退出当前轮询阶段有两个条件:
一旦符合以上两个条件之中的一个,event loop就会退出轮询阶段,进入check阶段。
从上面的描述,咱们能够看出,轮询阶段跟timer阶段和immediate阶段是有某种关系的。它们之间的关系能够用下面的流程图来体现:
正如上面给出的流程图所描述的那样,当poll处于空闲状态的时候(也就是I/Ocallback queue为空的时候),一旦event loop发现immediate callback queue有callback入队了,event loop就会退出轮询阶段,立刻进入check阶段。
调用setImmediate()时传入的callback会被传入到immediate callback queue中。event loop会依次执行队列中的callback,直到队列为空,才会移步到下一个阶段。
setImmediate()其实是执行在另外一个阶段的timer。在内部实现里面,它是利用libuv的一个负责调度代码的接口来实如今poll阶段以后执行相应的代码。
执行那些注册在关闭事件上callback的阶段。好比说:socket.on('close',callback)。这种类型的异步代码比较少,就不展开阐述了。
正如上面小节所解释的,这六个阶段里面,pending callbacks和idle/prepare这两个阶段是nodejs内部在使用的,只有四个阶段跟用户代码是相关的。咱们的异步代码最终是被推入到这四个阶段所对应的callback queue里面的。因此event loop自己有着如下的几个队列:
除了event loop的四个队列以外,还有两个队列值得咱们注意:
这两个队列虽然不属于event loop里面的,可是它们同样属于nodejs异步机制的一部分。若是以event loop机制所涉及的这六个队列为视角的话,event loop运行机制能够用下面的示意图来描述:
当nodejs程序的入口文件,也就是上图中的mainline code执行完毕后,在进入event loop以前是前后执行next tick callback和micortask callback的。有的技术文章将next tick callback归为microtask callback,二者是共存在一个队列里面,并强调它的优先级比诸如promise之类的其余microtask的优先级高。也有的技术文章强调二者是分别归属为不一样的队列,nodejs先执行next tick queue,再执行microtask callback queue。不管是哪种,所描述的运行结果都是同样的。显然,本文更同意采用后者。
调用process.nextTick()后,callback会入队到next tick callback queue中。调用Promise/then()后,相应的callback会进入microtask callback queue中。即便这两个队列同时不为空,nodejs老是先执行next tick callback queue,直到整个队列为空后,才会执行microtask callback queue。当microtask callback queue为空后,nodejs会再次回去检查next tick callback queue。只有当这两个队列都为空的状况下,nodejs才会进入event loop。 认真观察的话,咱们会发现,这两个队列的支持递纳入队的特性跟浏览器的event loop中micrtask队列是同样的。从这个角度,有些技术文章把next tick callback称为microtask callback是存在合理性的。当对microtask callback无限递纳入队时,会形成一个后果:event loop starvation。也便是会阻塞event loop。虽然,这个特性不会形成nodejs程序报调用栈溢出的错误,可是实际上,nodejs已经处于没法假死的状态了。因此,咱们不推荐无限递纳入队。
能够看出,next tick callback和microtask callback的执行已经造成了一个小循环,nodejs只有跳转这个小循环,才会进入event loop这个大循环。
当mainline code执行完毕后,nodejs也进入了event loop以后,假如此时timer callback queue和 immediate callback queue都不为空的时候,那应该先执行谁呢?你可能以为确定是执行timer callback queue啊。是的,正常状况下是会这样的。由于timer阶段在check阶段以前嘛。可是存在一种状况,是会先执行immediate callback queue,再执行timer callback queue。什么状况呢?那就是二者的入队动做发生在poll阶段(也能够说发生在I/O callback代码里面)。为何?由于poll阶段处于idle状态后,event loop一旦发现你immediate callback queue有callback了,它就会退出轮询阶段,从而进入check阶段去执行全部的immediate callback。此处不会像进入poll阶段以前所发生阶段回退,即不会优先回退到timer阶段去执行全部的timer callback。其实,timer callback的执行已是发生在下一次event loop里面了。综上所述,若是timer callback和immediate callback在I/O callback里面同时入队的话,event loop老是先执行后者,再执行前者。
假如在mainline code有这样代码:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
复制代码
那必定是先打印“timeout”,后打印“immediate”吗?答案是不必定。由于timer callback的入队时间点有可能受到进程性能(机器上运行中的其余应用程序会影响到nodejs应用进程性能)的影响,从而致使在event loop进入timer阶段以前,timer callback没能如预期进入队列。这个时候,event loop就已经进入了下一个阶段了。因此,上面的代码的打印顺序是没法保证的。有时候是先打印“timeout”,有时候是先打印“immediate”:
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
复制代码
大循环指的就是event loop,小循环就是指由next tick callback queue和microtask callback queue所组成的小循环。咱们能够下这么一个结论:一旦进入大循环以后,每执行完一个大循环 callback以后,就必须检查小循环。若是小循环有callback要执行,则须要执行完全部的小循环calback以后才会回归到大循环里面。 注意,这里强调的是,nodejs不会把event loop中当前阶段的队列都清空以后才进入小循环,而是执行了一个callback以后,就进入了小循环了。关于这一点,官方文档是这么说的:
......This is because process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop. Here, an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.
注意:在node v11.15.0以前(不包括自己),在这一点上是不同的。在这些版本里面,表现是:event loop执行完当前阶段callback queue里面的全部callback才会进入小循环。你能够在runkit上面验证一下。
为了帮助咱们理解,请看下面代码:
setImmediate(() => console.log('this is set immediate 1'));
setImmediate(() => {
Promise.resolve().then(()=>{
console.log('this is promise1 in setImmediate2');
});
process.nextTick(() => console.log('this is process.nextTick1 added inside setImmediate2'));
Promise.resolve().then(()=>{
console.log('this is promise2 in setImmediate2');
});
process.nextTick(() => console.log('this is process.nextTick2 added inside setImmediate2'));
console.log('this is set immediate 2')
});
setImmediate(() => console.log('this is set immediate 3'));
复制代码
若是是一次性执行完全部的immediate callback才进入小循环的话,那么打印结果应该是这样的:
this is set immediate 1
this is set immediate 2
this is set immediate 3
this is process.nextTick1 added inside setImmediate2
this is process.nextTick2 added inside setImmediate2
this is promise1 in setImmediate2
this is promise2 in setImmediate2
复制代码
可是实际打印结果是这样的:
看到没,在执行完第二个immediate以后,小循环已经有callback在队列里面了。这时候,nodejs会优先执行小循环里面的callback。假若小循环经过递纳入队造成了无限循环的话,那么就会出现上面所提到的“event loop starvation”。上面的示例代码只是拿immediate callback作个举例而已,对于event loop其余队列里面的callback也是同样的,在这里就不赘述了。
也许你会好奇,若是在小循环的callback里面入队小循环callback(也就是说递纳入队),那会怎样呢?也就是下面的代码的运行结果会是怎样呢?
process.nextTick(()=>{
console.log('this is process.nextTick 1')
});
process.nextTick(()=>{
console.log('this is process.nextTick 2')
process.nextTick(() => console.log('this is process.nextTick added inside process.nextTick 2'));
});
process.nextTick(()=>{
console.log('this is process.nextTick 3')
});
复制代码
运行结果以下:
this is process.nextTick 1
this is process.nextTick 2
this is process.nextTick 3
this is process.nextTick added inside process.nextTick 2
复制代码
能够看出,递纳入队的callback并不会插队到队列的中间,而是被插入到队列的末尾。这个表现跟在event loop中被入队的表现是不同的。这就是大循环和小循环在执行入队next tick callback和microtask callback时候的区别。
这二者之间有相同点,也有差别点。再次强调,如下结论是基于node v12.16.2来得出的。
从运行机制的实质上来看,二者大致上是没有什么区别的。具体展开来讲就是:若是把nodejs event loop中的mainline code和各个阶段中的callback都概括为macrotask callback,把next tick callback和其余诸如Promise/then()的microtask callback都概括为microtask callback的话,这两个event loop机制大致是一致的:都是先执行一个macrotask callback,再执行一个完整的microtask callback队列。microtask callback都具有递纳入队的特性,无限递纳入队都会产生“event loop starvation”后果。只有执行完microtask callback queue中的全部callback,才会执行下一个macrotask callback。
从技术细节来看,这二者仍是有几个不一样点: