node和浏览器对事件循环的处理,nodejs中的事件循环的执行顺序

  node和浏览器对事件循环的处理,nodejs中的事件循环的执行顺序

  Node.js是一种单线程语言,通过事件循环处理非阻塞I/O操作。下面这篇文章带你详细了解Node中的事件循环,希望对你有帮助!

  node.js速度课程简介:进入学习

  Node.js作为JavaScript的服务器运行时,主要处理网络和文件,没有浏览器中事件循环的渲染阶段。

  浏览器中有HTML规范来定义事件循环的处理模型,然后由各个浏览器厂商来实现。Node.js中事件循环的定义和实现来自于Libuv。

  Libuv是围绕事件驱动的异步I/O模型设计的,最初是为Node.js编写的,它提供了一个跨平台的支持库。下图显示了其组件。网络I/O是与网络处理相关的部分。右边是文件操作和DNS。在底层,epoll、kqueue、事件端口和IOCP是不同操作系统的实现。

  

事件循环的六个阶段

   node . js启动时会初始化事件循环,处理提供的脚本,同步代码直接在栈上执行。异步任务(网络请求、文件操作、定时器等。)会在调用API交付回调函数后将操作转移到后台由系统内核处理。目前大部分内核都是多线程的。当其中一个操作完成时,内核通知Node.js将回调函数添加到轮询队列中,并等待执行的机会。

  下图是Node.js官网对事件周期流程的描述,右边是Libuv官网对Node.js的描述,都是对事件周期的介绍。不是每个人都能去源码的。这两个文档通常是学习事件周期比较直接的参考文档,Node.js官网中的介绍相当详细,可以作为学习的参考。

  左边Node.js官网显示的事件循环分为六个阶段,每个阶段都有一个FIFO(先进先出)队列来执行回调函数。这些阶段的优先执行顺序仍然很清楚。

  右边更详细的描述了在事件循环迭代之前,先判断循环是否是活动的(有等待的异步I/O,定时器等。).如果它是活动的,开始迭代,否则循环将立即退出。

  下面将分别讨论每个阶段。

  

timers(定时器阶段)

  首先,事件循环进入定时器阶段,该阶段包含两个API,SetTimeout (CB,ms)和setInterval(cb,MS)。前者只执行一次,后者重复执行。

  在这个阶段,我们检查是否有过期的定时器函数,如果有,我们将执行过期的定时器回调函数。和浏览器中一样,定时器函数的延迟时间总是比我们预期的要晚,而且会受到操作系统或者其他正在运行的回调函数的影响。

  例如,在下面的例子中,我们设置了一个定时器函数,该函数预计在1000毫秒后执行。

  const now=date . now();

  setTimeout(函数定时器1(){

  log(` delay $ { date . now()-now } ms `);

  }, 1000);

  setTimeout(函数定时器2(){

  log(` delay $ { date . now()-now } ms `);

  }, 5000);

  some operation();

  函数someOperation() {

  //同步操作.

  while (Date.now() - now 3000) {}

  }调用setTimeout异步函数时,程序立即执行someOperation()函数,中间一些耗时的操作大概需要3000ms。当这些同步操作完成时,它进入一个事件循环。首先检查定时器阶段是否有过期的任务,定时器的脚本按照延迟时间升序存储在堆内存中。首先,检查超时最小的计时器功能。如果nowTime - timerTaskRegisterTime delay取出回调函数执行,否则继续检查。当检查到未到期的定时器功能或达到系统依赖关系的最大数量时,进入下一阶段。

  在我们的示例中,假设someOperation()函数完成的当前时间是T 3000:

  检查定时器1的功能。当前时间为T 3000-T 1000,已经超过预期的延迟时间。取出回调函数执行,继续检查。

  检查定时器2功能。当前时间为T 3000-T 5000,未达到预期延迟时间。此时退出计时器阶段。

  

pending callbacks

  计时器阶段完成后,事件循环进入挂起回调阶段,在该阶段中,执行上次事件循环遗留的I/O回调。根据Libuv文档的描述:在大多数情况下,所有的I/O回调都是在I/O轮询之后立即被调用,但是在某些情况下,调用这样的回调会延迟到下一次循环迭代。听完,更像是上一个阶段的遗留。

  

idle, prepare

  空闲,准备阶段供系统内部使用。Idle这个名字很容易混淆。虽然它被称为idle,但是当它们活动时,它将在每个事件循环中被调用。关于这一块的信息不多。省略.

  

poll

  民意调查是一个重要的阶段。这里有一个概念上的观察者,包括文件I/O观察者,网络I/O观察者等。它将观察是否有新的请求进来,包括读取文件和等待响应,以及等待新的套接字请求。在某些情况下,此阶段将会阻塞。

  

阻塞 I/O 超时时间

  在阻塞I/O之前,要计算它应该阻塞多长时间,请参考Libuv文档中的一些描述。以下是计算超时的规则:

  如果循环使用UV_RUN_NOWAIT标志,超时值为0。

  如果循环将要停止(调用uv_stop()),超时值为0。

  如果没有活动的处理程序或请求,超时值为0。

  如果有任何空闲处理程序处于活动状态,超时值为0。

  如果有任何要关闭的处理程序,超时值为0。

  如果上述条件都不存在,将使用最新定时器的超时,或者如果没有活动定时器,超时将是无限的,轮询阶段将一直被阻塞。

  

示例一

  一段非常简单的代码。我们启动一个服务器。现在,在事件周期的其他阶段没有要处理的任务。它将在这里等待,直到新的请求到来。

  const http=require( http );

  const server=http . create server();

  server.on(request ,req={

  console . log(req . URL);

  })

  server . listen(3000);

示例二

  结合第一阶段的计时器,我在看一个例子。首先启动app.js作为服务器,模拟3000ms响应延迟。这只是为了配合测试。再次运行client.js,查看事件循环的执行过程:

  首先,程序调用一个计时器,该计时器在1000毫秒后超时。

  之后,调用异步函数someAsyncOperation()从网络中读取数据。我们假设这个异步网络读取需要3000毫秒。

  当事件周期开始时,先进入定时器阶段,如果发现没有超时的定时器功能,继续向下执行。

  在等待回调-空闲期间,当准备进入轮询阶段时,http.get()此时还没有完成,其队列为空。参考上面的轮询阻塞超时规则,事件循环机制将检查最快达到阈值的计时器,而不是一直在这里等待。

  大约1000ms后,进入下一个事件循环进入定时器,执行过期定时器回调函数,我们会看到1003 ms后运行log setTimeout

  定时器阶段结束后,将再次进入轮询阶段,继续等待。

  //client.js

  const now=date . now();

  setTimeout(()=log(` setTimeout run after $ { date . now()-now } ms `),1000);

  someasynoperation();

  函数someasyncooperation(){

  http . get( http://localhost:3000/API/news ,()={

  log(`在${Date.now() - now} ms后获取数据成功);

  });

  }

  //app.js

  const http=require( http );

  http.createServer((req,res)={

  setTimeout(()={ res.end(OK!) }, 3000);

  }).听(3000);当轮询阶段队列为空且脚本已由setImmediate()调度时,事件循环也将结束轮询阶段并进入下一阶段检查。

  

check

  检查检查阶段在轮询阶段之后运行,它包含API setImmediate(cb)。如果有setImmediate触发的回调函数,就会取回来执行,直到队列为空或者达到系统的最大限制。

  

setTimeout VS setImmediate

  比较setTimeout和setImmediate,这是一个常见的例子。根据被调用的时间和计时器,它可能会受到计算机上其他正在运行的应用程序的影响,并且它们的输出顺序并不总是固定的。

  setTimeout(()=log( setTimeout ));

  set immediate(()=log( set immediate ));

  //首次运行

  定时器

  setImmediate

  //第二次运行

  setImmediate

  SetTimeout

setTimeout VS setImmediate VS fs.readFile

  但是一旦这两个函数在一个I/O循环中被调用,setImmediate将总是首先被调用。因为setImmediate属于检查阶段,所以它总是在事件循环中的轮询阶段之后运行,这个顺序是确定的。

  fs.readFile(__filename,()={

  setTimeout(()=log( setTimeout ));

  set immediate(()=log( set immediate ));

  })

close callbacks

  在Libuv中,如果调用关闭句柄uv_close(),就会调用关闭回调,这是事件循环的最后一个阶段,关闭回调。

  这个阶段的工作更像是做一些清洁工作。比如调用socket.destroy()的时候,这个阶段会发出 close 事件。事件循环在此阶段执行队列中的回调函数后,它会检查循环是否仍处于活动状态。如果为否,则退出,否则继续下一个新的事件循环。

  包含 Microtask 的事件循环流程图

  在浏览器的事件循环中,任务分为task和Microtask,在Node.js中是按照阶段来划分的,上面我们介绍了Node.js的事件循环的六个阶段,主要是定时器、轮询、检查、关闭回调四个阶段供用户使用,剩下的两个是系统调度的。这些阶段产生的任务可以看作是任务源,通常称为“Macrotask宏任务”。

  通常,当我们谈论一个事件循环时,我们也包括微任务。Node.js中的微任务包括Promise,以及一个可能很少被关注的函数queueMicrotask。它是在Node.js v11.0.0之后实现的,参见PR/22951。

  Node.js中的事件循环在各阶段执行完毕后,会检查微任务队列中是否有任务要执行。

  

Node.js 11.x 前后差异

   node . js v11 . x前后,如果同时存在可执行任务和微任务,那么每个阶段都会有一些差异。首先,看一段代码:

  setImmediate(()={

  log( set immediate 1 );

  Promise.resolve(Promise微任务1 )。然后(日志);

  });

  setImmediate(()={

  log( set immediate 2 );

  Promise . resolve(“Promise微任务2”)。然后(日志);

  });Node.js v11.x之前,如果当前阶段有多个可执行的任务,应该在开始执行微任务之前完成。基于v10.22.1的运行结果如下:

  setImmediate1

  setImmediate2

  承诺微任务1

  Promise微任务2在node.jsv11.x之后,如果当前阶段有多个可执行任务,先取出一个任务执行,清空对应的微任务队列,再取出下一个可执行任务继续执行。基于v14.15.0的运行结果如下:

  setImmediate1

  承诺微任务1

  setImmediate2

  Node.js v11.x之前Promise微任务2的这个执行顺序问题,被认为是v11.x及其执行时序修改后应该修复的Bug,与浏览器保持一致。详情见issues/22257。

  

特别的 process.nextTick()

   Node.js还有一个异步函数process.nextTick()。从技术上讲,它不是事件循环的一部分,它是在当前操作完成后处理的。如果有递归的process.nextTick()调用,就不好了,会阻塞事件循环。

  如下例所示,显示了process.nextTick()的递归调用示例。目前,事件循环位于I/O循环中。执行同步代码时,会立即执行process.nextTick(),陷入无限循环。与同步递归不同,它不会触及v8最大调用堆栈限制。但是它会破坏事件循环调度,setTimeout永远不会被执行。

  fs.readFile(__filename,()={

  process.nextTick(()={

  log( next tick );

  run();

  函数运行(){

  process . next tick(()=run());

  }

  });

  日志(“同步运行”);

  setTimeout(()=log( setTimeout ));

  });

  //输出

  同步运行

  NextTick更改process.nextTick到setImmediate是递归的,但不会影响事件循环调度。setTimeout将在下一个事件周期中执行。

  fs.readFile(__filename,()={

  process.nextTick(()={

  log( next tick );

  run();

  函数运行(){

  set immediate(()=run());

  }

  });

  日志(“同步运行”);

  setTimeout(()=log( setTimeout ));

  });

  //输出

  同步运行

  下一滴答

  立即执行SetTimeoutprocess.nextTick,在下一个事件周期的检查阶段执行setImmediate。但是,他们的名字真的让人摸不着头脑。也许交换这两个名字会更好,但这是一个遗留问题,不太可能改变,因为它会破坏NPM的大部分软件包。

  在Node.js的文档中,也建议开发者尽量使用setImmediate(),这样更容易理解。

  更多关于node的信息,请访问:nodejs教程!也就是上面的文章深入了解了Node中事件循环的细节。请多关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: