在最爱源码网可以下载免费的程序源码资源,欢迎大家分享投稿!!

图文结合带你搞懂Nodejs中的事件循环

本篇文章通过图文结合的形式来带大家搞懂Nodejs中的事件循环,希望对大家有所帮助!

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

以下全文7000字,请在你思路清晰、精力充沛的时刻观看。保证你理解后很长时间忘不掉。【推荐学习:《nodejs 教程》】

Node事件循环

Node底层使用的语言libuv,是一个c++语言。他用来操作底层的操作系统,封装了操作系统的接口。Node的事件循环也是用libuv来写的,所以Node生命周期和浏览器的还是有区别的。

因为Node和操作系统打交道,所以事件循环比较复杂,也有一些自己特有的API。
事件循环在不同的操作系统里有一些细微的差异。这将涉及到操作系统的知识,暂时不表。 本次只介绍JS主线程中,Node的运作流程。Node的其他线程暂时也不扩展。

事件循环图

说好的一张图,也不卖关子。下边这张图搞清楚了,事件循环就学会了。

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

事件循环图

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

事件循环图-结构

为了让大家先有个大局观,先贴一张目录结构图在前边:

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

目录

接下来详细展开说说

主线程

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

主线程

上图中,几个色块的含义:

  • main:启动入口文件,运行主函数
  • event loop:检查是否要进入事件循环
    • 检查其他线程里是否还有待处理事项
    • 检查其他任务是否还在进行中(比如计时器、文件读取操作等任务是否完成)
    • 有以上情况,进入事件循环,运行其他任务
      事件循环的过程:沿着从timers到close callbacks这个流程,走一圈。到event loop看是否结束,没结束再走一圈。
  • over:所有的事情都完毕,结束

事件循环 圈

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

事件循环 圈

图中灰色的圈跟操作系统有关系,不是本章解析重点。重点关注黄色、橙色的圈还有中间橘黄的方框。

我们把每一圈的事件循环叫做「一次循环」、又叫「一次轮询」、又叫「一次Tick」。

一次循环要经过六个阶段:

  • timers:计时器(setTimeout、setInterval等的回调函数存放在里边)
  • pending callback
  • idle prepare
  • poll:轮询队列(除timers、check之外的回调存放在这里)
  • check:检查阶段(使用 setImmediate 的回调会直接进入这个队列)
  • close callbacks

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

次我们只关注上边标红的三个重点。

工作原理

  • 每一个阶段都会维护一个事件队列。可以把每一个圈想象成一个事件队列。
  • 这就和浏览器不一样了,浏览器最多两个队列(宏队列、微队列)。但是在node里边有六个队列
  • 到达一个队列后,检查队列内是否有任务(也就是看下是否有回调函数)需要执行。如果有,就依次执行,直到全部执行完毕、清空队列。
  • 如果没有任务,进入下一个队列去检查。直到所有队列检查一遍,算一个轮询。
  • 其中,timerspending callbackidle prepare等执行完毕后,到达poll队列。

timers队列的工作原理

timers并非真正意义上的队列,他内部存放的是计时器。
每次到达这个队列,会检查计时器线程内的所有计时器,计时器线程内部多个计时器按照时间顺序排序。

检查过程:将每一个计时器按顺序分别计算一遍,计算该计时器开始计时的时间到当前时间是否满足计时器的间隔参数设定(比如1000ms,计算计时器开始计时到现在是否有1m)。当某个计时器检查通过,则执行其回调函数。

poll队列的运作方式

  • 如果poll中有回调函数需要执行,依次执行回调,直到清空队列。
  • 如果poll中没有回调函数需要执行,已经是空队列了。则会在这里等待,等待其他队列中出现回调,
    • 如果其他队列中出现回调,则从poll向下到over,结束该阶段,进入下一阶段。
    • 如果其他队列也都没有回调,则持续在poll队列等待,直到任何一个队列出现回调后再进行工作。(是个小懒虫的处事方式)

举例梳理事件流程

setTimeout(() => {   console.log('object'); }, 5000) console.log('node');

以上代码的事件流程梳理

  • 进入主线程,执行setTimeout(),回调函数作为异步任务被放入异步队列timers队列中,暂时不执行。
  • 继续向下,执行定时器后边的console,打印“node”。
  • 判断是否有事件循环。是,走一圈轮询:从timers – pending callback – idle prepare……
  • poll队列停下循环并等待。
    • 由于这时候没到5秒,timers队列无任务,所以一直在poll队列卡着,同时轮询检查其他队列是否有任务。
  • 等5秒到达,setTimeout的回调塞到timers内,例行轮询检查到timers队列有任务,则向下走,经过check、close callbacks后到达timers。将timers队列清空。
  • 继续轮询到poll等待,询问是否还需要event loop,不需要,则到达over结束。

要理解这个问题,看下边的代码及流程解析:

setTimeout(function t1() {   console.log('setTimeout'); }, 5000) console.log('node 生命周期');  const http = require('http')  const server = http.createServer(function h1() {   console.log('请求回调'); });  server.listen(8080)

代码分析如下:

  • 照旧,先执行主线程,打印“node 生命周期”、引入http后创建http服务。
  • 然后event loop检查是否有异步任务,检查发现有定时器任务和请求任务。所以进入事件循环。
  • 六个队列都没任务,则在poll队列等待。如下图:

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

  • 过了五秒,timers中有了任务,则流程从poll放行向下,经过check和close callbacks队列后,到达event loop。
  • event loop检查是否有异步任务,检查发现有定时器任务和请求任务。所以再次进入事件循环。
  • 到达timers队列,发现有回调函数任务,则依次执行回调,清空timers队列(当然这里只有一个5秒到达后的回调,所以直接执行完了即可),打印出“setTimeout”。如下图

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

  • 清空timers队列后,轮询继续向下到达poll队列,由于poll队列现在是空队列,所以在这里等待。
  • 后来,假设用户请求发来了,h1回调函数被放到poll队列。于是poll中有回调函数需要执行,依次执行回调,直到清空poll队列。
  • poll队列清空,此时poll队列是空队列,继续等待。

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

  • 由于node线程一直holding在poll队列,等很长一段时间还是没有任务来临时,会自动断开等待(不自信表现),向下执行轮询流程,经过check、close callbacks后到达event loop
  • 到了event loop后,检查是否有异步任务,检查发现有请求任务。(此时定时器任务已经执行完毕,所以没有了),则继续再次进入事件循环。
  • 到达poll队列,再次holding……
  • 再等很长时间没有任务来临,自动断开到even loop(再补充一点无任务的循环情况)
  • 再次回到poll队列挂起
  • 无限循环……

梳理事件循环流程图:

注意:下图中的“是否有任务”的说法表示“是否有本队列的任务”。

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

event loop流程梳理

再用一个典型的例子验证下流程:

const startTime = new Date();  setTimeout(function f1() {   console.log('setTimeout', new Date(), new Date() - startTime); }, 200)  console.log('node 生命周期', startTime);  const fs = require('fs')  fs.readFile('./poll.js', 'utf-8', function fsFunc(err, data) {   const fsTime = new Date()   console.log('fs', fsTime);   while (new Date() - fsTime < 300) {   }   console.log('结束死循环', new Date()); });

连续运行三遍,打印结果如下:

%title插图%num最爱源码网图文结合带你搞懂Nodejs中的事件循环

执行流程解析:

  • 执行全局上下文,打印「node 生命周期 + 时间」
  • 询问是否有event loop
  • 有,进入timers队列,检查没有计时器(cpu处理速度可以,这时还没到200ms)
  • 轮询进入到poll,读文件还没读完(比如此时才用了20ms),因此poll队列是空的,也没有任务回调
  • 在poll队列等待……不断轮询看有没有回调
  • 文件读完,poll队列有了fsFunc回调函数,并且被执行,输出「fs + 时间」
  • 在while死循环那里卡300毫秒,
  • 死循环卡到200ms的时候,f1回调进入timers队列。但此时poll队列很忙,占用了线程,不会向下执行。
  • 直到300ms后poll队列清空,输出「结束死循环 + 时间」
  • event loop赶紧向下走
  • 再来一轮到timers,执行timers队列里的f1回调。于是看到「setTimeout + 时间」
  • timers队列清空,回到poll队列,没有任务,等待一会。
  • 等待时间够长后,向下回到event loop。
  • event loop检查没有其他异步任务了,结束线程,整个程序over退出。

check 阶段

检查阶段(使用 setImmediate 的回调会直接进入这个队列)

check队列的实际工作原理

真正的队列,里边扔的就是待执行的回调函数的集合。类似[fn,fn]这种形式的。
每次到达check这个队列后,立即按顺序执行回调函数即可【类似于[fn1,fn2].forEach((fn)=>fn())的感觉】

所以说,setImmediate不是一个计时器的概念。

如果你去面试,涉及到Node环节,可能会遇到下边这个问题:setImmediate和setTimeout(0)谁更快。

setImmediate() 与 setTimeout(0) 的对比

  • setImmediate的回调是异步的,和setTimeout回调性质一致。
  • setImmediate回调在check队列,setTimeout回调在timers队列(概念意义,实际在计时器线程,只是setTimeout在timers队列做检查调用而已。详细看timers的工作原理)。
  • setImmediate函数调用后,回调函数会立即push到check队列,并在下次eventloop时被执行。setTimeout函数调用后,计时器线程增加一个定时器任务,下次eventloop时会在timers阶段里检查判断定时器任务是否到达时间,到了则执行回调函数。
  • 综上,setImmediate的运算速度比setTimeout(0)的要快,因为setTimeout还需要开计时器线程,并增加计算的开销。

二者的效果差不多。但是执行顺序不定

观察以下代码:

setTimeout(() => {   console.log('setTimeout'); }, 0);  setImmediate(() => {   console.log('setImmediate'); });

多次反复运行,执行效果如下:

网站侵权说明: 本站采用 CC BY-NC-SA 4.0 国际许可协议 进行许可,转载或引用本站文章应遵循相同协议。
1 本站为转载分享站点,不提供任何上传下载服务,所有内容均来自互联网第三方分享站点所提供的公开引用内容,不需要任何付费即可公开阅读。
2 本站提供的一切软件、教程和内容信息仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。
3 本站信息来自网络收集整理,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
4 如果您喜欢该程序和内容,请支持正版,购买注册,得到更好的正版服务。我们非常重视版权问题,如有侵权请邮件与我们联系处理。敬请谅解!我们会在24h内删除有争议的资源。

最爱源码网 » 图文结合带你搞懂Nodejs中的事件循环
升级VIP尊享更多特权立即升级