此处如无非凡指出的话,event loop的语境都是指nodejs

本文研究所用的nodejs环境是:操作系统window10 + nodejs版本号为v12.16.2

什么是event loop?

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是单线程的)”的意思。

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.

nodejs的架构

在持续探讨nodejs event loop之前,咱们无妨来看看nodejs的架构图:

从下面的架构图,你能够看出,libuv是位于架构的最底层的。而咱们所要讲得event loop的实现是由libuv来提供的。当初,你的脑海外面应该有一幅残缺的画面,并分明地晓得event loop到底处在哪个地位了。

这里值得强调的一点是,无论是chrome浏览器中的还是nodejs中的event loop,其实都不是由v8引擎来实现的。

对于event loop几个误会

误会1:event loop和用户代码别离跑在不同的线程上

常常听到这样的说法,用户的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是什么关系呢?咱们能够看看上面的源代码:

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运行所在的那一个线程。”

误会2:所有的异步操作都是交给libuv的线程池(thread pool)来实现的

异步操作,比方像文件系统的读写,收回HTTP申请或者对数据库进行读写等等都是load off给libuv的线程池来实现的。

假相

libuv的确会创立一个具备四个线程的线程池。然而,时至今日,许多操作系统曾经向外提供一些实现异步I/O的接口了(例如:Linux下面的AIO),libuv外部会优先思考应用这些现成的API接口来实现异步I/O。只有在特定状况下(某个操作系统对某种类型I/O没有提供相应的异步接口的时候),libuv才会应用线程池中的线程+轮询来实现异步I/O。

误会3: event loop就是一个stack或者queue

event loop会继续地以一种FIFO的形式遍历一个装满着异步task callback的队列,当这个task实现之后,event loop就会执行它相应的callback。

假相

event loop机制中的确是波及到相似于队列的数据结构,然而并不是只有一个这种“队列”。实际上,event loop次要遍历的是不同的阶段(phase),每个阶段会有一个装着callback函数的队列与之绝对应(称之为callback queue)。当执行到某个阶段的时候,event loop才会去遍历这个阶段所对应的callback queue。

event loop六个阶段

首先,咱们从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开展来看的的时候,实际上它次要是包含六个阶段:

  1. timers
  2. pending callbacks
  3. idle/prepare
  4. poll
  5. check。
  6. close callbacks

event loop会顺次进入上述的每个阶段。每个阶段都会有一callback queue与之绝对应。event loop会遍历这个callback queue,执行外面的每一个callback。直到callback queue为空或者以后callback的执行数量超过了某个阈值为止,event loop才会移步到下一个阶段。

1. timers

在这个阶段,event loop会查看是否有到期的定时器能够执行。如果有,则执行。调用setTimeout或者setInterval办法时传入的callback会在指定的延迟时间后入队到timers callback queue 。跟浏览器环境中的setTimeout和setInterval办法一样,调用时候传入的延迟时间并不是回调确切执行的工夫。timer callback的执行工夫点无奈失去稳固的,统一的保障,因为它们的执行会受到操作系统调度层面和其余callback函数调用耗时的影响。所以,对传入setTimeout或者setInterval办法的延迟时间参数正确的冀望是:在我指定的延迟时间后,nodejs啊,我心愿你尽快地帮我执行我的callback。也就是说timer callback函数的执行只会比咱们预约的工夫的要晚,不会比咱们预约的工夫要早。

从技术上来说,poll阶段理论管制了timer callback执行的工夫点。

参考nodejs进阶视频解说:进入学习

2. pending callbacks

这个阶段次要是执行某些零碎层级操作的回调函数。比如说,TCP产生谬误时候的谬误回调。如果一个TCP socket在尝试建设连贯的时候产生了“ECONNREFUSED”谬误,则nodejs须要将对应的谬误回调入队到pending callback queue中,并马上执行,以此来告诉操作系统。

3. idle/prepare

只供nodejs外部来用的阶段。对于开发者而言,简直能够疏忽。

poll

在进入轮询阶段之前,event loop会查看timer callback queue是否为空,如果不为空的话,那么event loop就会回退到timer阶段,顺次执行所有的timer callback才回到轮询阶段。

进入轮询阶段后,event loop会做两件事:

  1. 依据不同的操作系统的理论状况来计算轮询阶段所应该占用event loop的工夫长度。
  2. 对Event Demultiplexer进行轮询,并执行I/O callback queue外面的callback。

因为nodejs是志在利用于I/O密集型软件,所以,在一个event loop循环中,它会破费很大比例的工夫在轮询阶段。在这个阶段,event loop要么处于执行I/O callback状态,要么处于轮询期待的状态。当然,轮询阶段占用event loop的工夫也会是有个限度的。这就是第一件事件要实现的事-计算出有一个切合以后操作系统环境的适宜的最大工夫值。event loop退出以后轮询阶段有两个条件:

  1. 条件一:以后轮询阶段所占用的工夫长度曾经超过了nodejs计算出来的阈值。
  2. 条件二:当天I/O callback queue曾经为空,并且immediate callback不为空。

一旦合乎以上两个条件之中的一个,event loop就会退出轮询阶段,进入check阶段。

从下面的形容,咱们能够看出,轮询阶段跟timer阶段和immediate阶段是有某种关系的。它们之间的关系能够用上面的流程图来体现:

check

正如下面给出的流程图所形容的那样,当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阶段之后执行相应的代码。

close callback

执行那些注册在敞开事件上callback的阶段。比如说:socket.on('close',callback)。这种类型的异步代码比拟少,就不开展论述了。

更多细节

正如下面大节所解释的,这六个阶段外面,pending callbacks和idle/prepare这两个阶段是nodejs外部在应用的,只有四个阶段跟用户代码是相干的。咱们的异步代码最终是被推入到这四个阶段所对应的callback queue外面的。所以event loop自身有着以下的几个队列:

  • timer callback queue
  • I/O callback queue
  • immediate callback queue
  • close callback queue

除了event loop的四个队列之外,还有两个队列值得咱们留神:

  • nextTick callback queue。调用process.nextTick()时传入的callback会被入队到这里。
  • microtask callback queue。一个promise对象reslove或者reject时传入的callback会被入队到这里。

这两个队列尽管不属于event loop外面的,然而它们一样属于nodejs异步机制的一部分。如果以event loop机制所波及的这六个队列为视角的话,event loop运行机制能够用上面的示意图来形容:

event loop示意图

process.nextTick()和Promise/then()

当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这个大循环。

setTimeout VS setImmediate()

当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.jssetTimeout(() => {  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.jstimeoutimmediate$ node timeout_vs_immediate.jsimmediatetimeout

大循环 VS 小循环

大循环指的就是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 1this is set immediate 2this is set immediate 3this is process.nextTick1 added inside setImmediate2this is process.nextTick2 added inside setImmediate2this is promise1 in setImmediate2this 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 1this is  process.nextTick 2this is  process.nextTick 3this is process.nextTick added inside process.nextTick 2

能够看出,递归入队的callback并不会插队到队列的两头,而是被插入到队列的开端。这个体现跟在event loop中被入队的体现是不一样的。这就是大循环和小循环在执行入队next tick callback和microtask callback时候的区别。

nodejs与browser中event loop的区别

这两者之间有相同点,也有差别点。再次强调,以下论断是基于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。

不同点

从技术细节来看,这两者还是有几个不同点:

  • 在nodejs event loop的实现中,没有macrotask的说法。
  • nodejs event loop 是依照阶段来划分的,具备六个阶段,对应六种类型的队列(其中两种是只供外部应用);而browser event loop不依照阶段划分,只有两种类型的队列,即macrotask queue和microtask queue。从另外一个角度咱们能够这么了解:nodejs event loop有2个microtask队列,有4个macrotask队列;而浏览器event loop只有1个microtask队列,有1个macrotask队列。
  • 最大的不同,在于nodejs evnet loop有个轮询阶段。当evnet loop中所有队列都为空的时候,browser event loop会退出event loop(或者说处于休眠状态)。然而nodejs event loop不一样,它会继续命中轮询阶段,并且在那里期待处于pending状态的I/O callback。只有等待时间超出了nodejs计算出来的限定工夫或者再也没有未实现的I/O工作的时候,nodejs才会退出event loop。这就是nodejs event loop跟browser event loop最大不同的中央。