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

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

  本文通过图文结合的方式带你了解Nodejs中的事件循环,希望对你有所帮助!

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

  全文7000字,请在您思路清晰、精力充沛时收看。理解了之后很长一段时间都忘不掉。[推荐研究:《nodejs 教程》]

  

Node事件循环

  节点底层使用的节点语言是libuv,是一种C语言。用于操作底层操作系统,封装操作系统的接口。Node的事件循环也是用libuv写的,所以Node和浏览器的生命周期还是有区别的。

  因为Node处理的是操作系统,所以事件周期比较复杂,也有一些独特的API。

  不同操作系统中的事件略有不同。这个会涉及到操作系统的知识,暂时不展示。

  这次只介绍JS主线程中Node的操作过程。Node的其他线程暂时不会扩展。

  

事件循环图

  好的画面不是悬念。下图清晰,事件循环学习。

  事件循环图

  事件图-结构

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

  目录

  具体说一下吧。

  

主线程

  主流中泓线

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

  Main:启动入口文件,运行主函数事件循环:检查是否要进入事件循环,检查其他线程是否有未决事项,检查其他任务是否还在进行中(如定时器、文件读取操作等任务完成)。在上述情况下,进入事件循环并运行其他任务。

  事件的循环过程:按照从定时器到关闭回调的过程,走来走去。转到事件循环,看是否结束。如果还没结束,再走一圈。结束:一切都结束了,结束

事件循环 圈

  

  事件循环

  图中的灰圈与操作系统有关,不是本章的重点。关注黄色和橙色的圆圈以及中间的橙色方框。

  我们把每个圆圈中的事件循环称为“一个周期”、“一次投票”和“一次滴答”。

  

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

  timers:计时器(setTimeout、setInterval等的回调函数存放在里边)

  等待回拨

  空闲准备

  poll:轮询队列(除timers、check之外的回调存放在这里)

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

  结束回访

  这一次,我们只关注上面标记为红色的三个关键点。

  

工作原理

  每个阶段将维护一个事件队列。把每个圆圈想象成一个事件队列。这与浏览器不同,浏览器最多有两个队列(宏队列和微队列)。但是,在node中的六个队列到达一个队列后,检查队列中是否有任务要执行(即检查是否有回调函数)。如果是,它将按顺序执行,直到所有的执行完成,队列被清空。如果没有任务,就去下一个队列检查。在检查完所有队列之前,它算作一次轮询。其中包括定时器、等待回调、空闲准备等。执行后到达轮询队列。

timers队列的工作原理

  计时器不是一个真正的队列,它在里面存储计时器。

  每次到达这个队列时,都会检查计时器线程中的所有计时器,计时器线程中的多个计时器按时间顺序排序。

  检查过程:依次计算每个定时器,计算定时器启动到当前时间是否满足定时器的间隔参数设置(例如1000ms,计算定时器启动到当前时间是否为1m)。当一个定时器通过检查时,它的回调函数被执行。

  

poll队列的运作方式

  如果poll中有回调函数需要执行,则依次执行回调,直到队列清空。如果poll中没有要执行的回调函数,那么它已经是一个空队列。会在这里等待,等待其他队列的回调。如果回调出现在其他队列中,它将从poll下降到over,结束此阶段并进入下一阶段。如果其他队列没有回调,它们将在轮询队列中等待,直到任何队列有回调。(很懒的做事方式)

举例梳理事件流程

   setTimeout(()={

  console . log(“object”);

  }, 5000)

  console . log(“node”);

以上代码的事件流程梳理

  进入主线程,执行setTimeout()。作为异步任务,回调函数被放入异步队列的timers队列中,暂时不会执行。往下走,执行定时器后面的控制台,打印“node”。确定是否存在事件循环。是的,进行一轮轮询:从计时器-等待回调-空闲准备.至poll队列,停止循环并等待。由于此时不到5秒,timers队列没有任务,所以一直卡在poll队列中,同时轮询检查其他队列是否有任务。5秒钟后,setTimeout的回调将被填充到计时器中。如果例程轮询检查计时器队列中是否有任务,它将在检查和关闭回调后到达计时器。清空计时器队列。继续轮询,直到轮询等待,并询问是否仍然需要事件循环。如果没有,它到达结束。

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

   setTimeout(函数t1() {

  console . log( settime out );

  }, 5000)

  Console.log(“节点生命周期”);

  const http=require(http )

  const server=http.createServer(函数h1() {

  Console.log(请求回调);

  });

  Server.listen(8080)代码分析如下:

  照例先执行主线程,打印“节点生命周期”,引入http后创建http服务。然后事件循环检查是否有异步任务,发现有定时器任务和请求任务。所以进入事件循环。如果六个队列中没有任务,请在轮询队列中等待。如下图:

  五秒钟后,当计时器中有任务时,流程从轮询开始,经过检查和关闭回调队列,到达事件循环。事件循环检查是否有异步任务,发现有定时器任务和请求任务。所以再次进入事件循环。到了timers队列,如果发现一个回调函数任务,就依次执行回调,清空timers队列(当然5秒到达后只有一个回调,直接完成即可),打印出“setTimeout”。如下图

  定时器队列清空后,轮询继续到轮询队列。因为轮询队列现在是空的,所以在这里等待。稍后,假设用户请求进来,h1回调函数被放在轮询队列中。所以poll中有回调函数要执行,回调依次执行,直到轮询队列清空。轮询队列为空。此时,轮询队列为空,因此继续等待。

  由于节点线程在轮询队列中持有时间较长,当没有任务到来时,会自动断开等待(表示没有信心),并下到轮询进程。在检查并关闭回调后,它将到达事件循环。当它到达事件循环时,它将检查是否有异步任务,并检查是否有请求的任务。(此时定时器任务已经完成,不再赘述),然后继续再次进入事件循环。到达投票队列,再次等待.长时间等待没有任务到来,自动断开到even循环(添加一点关于没有任务的循环),然后返回轮询队列再次挂起无限循环.

梳理事件循环流程图:

  注:下图中的语句“是否有任务”是指“该队列中是否有任务”。

  事件循环流程梳理

  

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

   const start time=new Date();

  setTimeout(函数f1() {

  console.log(setTimeout ,new Date(),new Date()-start time);

  }, 200)

  Console.log(节点生命周期,start time);

  const fs=require(fs )

  fs.readFile(。/poll.js , utf-8 ,函数fsFunc(err,data) {

  const fsTime=新日期()

  console.log(fs ,fs time);

  while(新日期()- fsTime 300) {

  }

  Console.log(结束无限循环,new Date());

  });连续运行三次,打印结果如下:

  

  执行流程解析:

  执行全局上下文并打印“节点生命周期时间”

  问是否有事件循环。

  可以,进入定时器队列,检查是否有定时器(cpu处理速度可以,但还没到200ms)。

  轮询成poll后,读取文件还没有完成(比如此时只用了20ms),所以轮询队列为空,没有任务回调。

  在轮询队列中等待.继续轮询,看是否有回调。

  读取文件后,poll queue有fsFunc回调函数,执行后输出“fs time”

  在while无限循环中,它停留了300毫秒,

  当死卡达到200ms时,f1回调进入定时器队列。但是此时轮询队列比较忙,占用了线程,不会向下执行。

  直到300ms后,轮询队列被清除,输出“无限循环时间结束”。

  事件循环,赶紧下来。

  再次进入计时器,在计时器队列中执行f1回调。所以我看到“暂停时间”

  计时器队列为空,请返回轮询队列,没有任务,请稍候。

  等待足够长的时间后,回到事件循环。

  事件循环检查没有其他异步任务。结束线程,整个程序结束。

  

check 阶段

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

  

check队列的实际工作原理

  实际队列,在其中抛出要执行的回调函数集。类似于[fn,fn]的东西。

  每次到了校验队列,就可以立即按顺序执行回调函数【类似于[fn1,fn2]的感觉】。forEach((fn)=fn ())]

  因此,setImmediate不是一个定时器概念。

  如果去面试,涉及到节点环节,可能会遇到以下问题:setImmediate和setTimeout(0)谁更快?

  

setImmediate() 与 setTimeout(0) 的对比

   set immediate的回调是异步的,这与setTimeout回调的性质是一致的。SetImmediate回调在check队列中,setTimeout回调在timers队列中(概念上的意思,其实是计时器线程,但是setTimeout在timers队列中进行check调用。详见定时器如何工作)。调用setImmediate函数后,回调函数将被立即推送到检查队列,并在下一个eventloop执行。调用setTimeout函数后,一个计时器任务被添加到计时器线程中。下一个eventloop将检查并确定计时器任务是否已经到达计时器阶段的时间,然后将执行回调函数。综上所述,setImmediate的运算速度比setTimeout(0)快,因为setTimeout还需要启动一个定时器线程,增加了计算开销。

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

  观察以下代码:

  setTimeout(()={

  console . log( settime out );

  }, 0);

  setImmediate(()={

  console . log( set immediate );

  });重复操作后,执行效果如下:

  不确定订单

  可以看到它运行了很多次,打印两句话console.log的顺序不确定。这是因为setTimeout的最小间隔数填充的是1,尽管下面的代码填充的是0。但实际电脑执行是1 ms,(这里注意和浏览器的定时器不一样。在浏览器中,setInterval的最小间隔为10ms如果小于10ms,则设置为10;设备通电时,最小间隔为16.6毫秒。)

  上面的代码中,主线程运行时,调用setTimeout函数,定时器线程添加一个定时器任务。调用setImmediate函数后,其回调函数会立即被推送到检查队列中。主线程已经完成执行。

  eventloop判断时,发现定时器和校验队列中有内容,进入异步轮询:

  第一种情况:在timers中等待后,可能不是1ms,定时器任务间隔的条件不成立,所以timers中没有回调函数。继续向下到检查队列。此时setImmediate的回调函数已经等待很久了,直接执行。而下一次eventloop到达timers队列的时候,定时器已经到期,就会执行setTimeout的回调任务。所以顺序是“setImmediate-setTimeout”。

  第二种情况:但是也有可能当它到达timers阶段时,超过了1ms。所以定时器计算条件成立,直接执行setTimeout的回调函数。然后,eventloop进入检查队列,执行setImmediate的回调。最后一个序列是“setTimeout-setImmediate”。

  所以,只比较这两个函数的情况下,二者的执行顺序最终结果取决于当下计算机的运行环境以及运行速度。

  

二者时间差距的对比代码

   -设置超时测试:-。

  设I=0;

  console . time( setTimeout );

  功能测试(){

  如果(i 1000) {

  setTimeout(测试,0)

  我

  }否则{

  console . time end( settime out );

  }

  }

  test();

  -设置立即测试:-

  设I=0;

  console . time( set immediate );

  功能测试(){

  如果(i 1000) {

  立即设置(测试)

  我

  }否则{

  console . time end( set immediate );

  }

  }

  test();观察运行时间差距:

  SetTimeout和setImmediate时间差

  可见setTimeout远比setImmediate耗时多得多

  这是因为setTimeout不仅需要时间来执行主代码。计时器队列中的计时器线程中还有每个计时任务的计算时间。

  

结合poll队列的面试题(考察timers、poll和check的执行顺序)

  如果你看懂了上面的事件循环图,下面这个问题就难倒你了!

  //告诉我们下面这段代码的执行顺序。先打印哪个?

  const fs=require(fs )

  fs.readFile( ./poll.js ,()={

  setTimeout(()=控制台。log( setTimeout ),0)

  set immediate(()=console。日志(立即设置))

  })上边这种代码逻辑,不管执行多少次,肯定都是先执行setImmediate。

  先执行setImmediate

  因为满量程各个函数的回调是放在投票队列的。当程序举办在投票队列后,出现回调立即执行。

  回调内执行定时器和setImmediate的函数后,检查队列立即增加了回调。

  回调执行完毕,轮询检查其他队列有内容,程序结束投票队列的举办向下执行。

  支票是投票阶段的紧接着的下一个。所以在向下的过程中,先执行支票阶段内的回调,也就是先打印setImmediate。

  到下一轮循环,到达定时器队列,检查定时器计时器符合条件,则定时器回调被执行。

  

nextTick 与 Promise

   说完宏任务,接下来说下微任务

  二者都是「微队列」,执行异步微任务。二者不是事件循环的一部分,程序也不会开启额外的线程去处理相关任务。(理解:承诺里发网络请求,那是网络请求开的网络线程,跟承诺这个微任务没关系)微队列设立的目的就是让一些任务「马上」、「立即」优先执行。下一滴答与承诺比较,下一个刻度的级别更高

nextTick表现形式

  流程。下一个滴答(()={ })

Promise表现形式

  承诺。解决().然后(()={})

如何参与事件循环?

  事件循环中,每执行一个回调前,先按序清空一次下一滴答和承诺。

  //先思考下列代码的执行顺序

  setImmediate(()={

  控制台。log(“set immediate”);

  });

  process.nextTick(()={

  控制台。log(下一个滴答1 );

  process.nextTick(()={

  控制台。log(下一个滴答2 );

  })

  })

  控制台。log("全局");

  Promise.resolve().然后(()={

  控制台。log(诺言1 );

  process.nextTick(()={

  控制台。日志(“承诺中的下一个刻度”);

  })

  })最终顺序:

  全球的

  下一滴答一

  下一个节拍2

  承诺一

  下一个承诺

  setImmediate

  

两个问题:

   基于上边的说法,有两个问题待思考和解决:

  每走一个异步宏任务队列就查一遍下一滴答和答应吗?还是每执行完宏任务队列里的一个回调函数就查一遍呢?

  如果在投票的举办阶段,插入一个下一滴答或者承诺的回调,会立即停止投票队列的举办去执行回调吗?

  上边两个问题,看下边代码的说法

  setTimeout(()={

  控制台。log( settime out 100 );

  setTimeout(()={

  控制台。log( setTimeout 100-0 );

  process.nextTick(()={

  控制台。日志(设置超时100-0中的下一个滴答’);

  })

  }, 0)

  setImmediate(()={

  控制台。日志(设置超时100中的set immediate’);

  process.nextTick(()={

  控制台。日志(设置超时100中setImmediate中的下一个滴答’);

  })

  });

  process.nextTick(()={

  控制台。日志(设置超时100中的下一个滴答’);

  })

  Promise.resolve().然后(()={

  控制台。日志(设置超时100中的承诺);

  })

  }, 100)

  const fs=require(fs )

  fs.readFile( ./1.poll.js ,()={

  控制台。log(" poll 1 ");

  process.nextTick(()={

  控制台。log(轮询中下一个刻度======);

  })

  })

  setTimeout(()={

  控制台。日志( settime out 0 );

  process.nextTick(()={

  控制台。log( settime out中的下一个刻度);

  })

  }, 0)

  setTimeout(()={

  控制台。log( settime out 1 );

  Promise.resolve().然后(()={

  控制台。log( promise in settime out 1 );

  })

  process.nextTick(()={

  控制台。日志(设置超时1中的下一个滴答’);

  })

  }, 1)

  setImmediate(()={

  控制台。log(“set immediate”);

  process.nextTick(()={

  控制台。日志(立即设置中的下一个滴答’);

  })

  });

  process.nextTick(()={

  控制台。log(下一个滴答1 );

  process.nextTick(()={

  控制台。log(下一个滴答2 );

  })

  })

  控制台。log("全局-");

  Promise.resolve().然后(()={

  控制台。log(诺言1 );

  process.nextTick(()={

  控制台。日志(“承诺中的下一个刻度”);

  })

  })

  /** 执行顺序如下

  全球-

  下一滴答一

  下一个节拍2

  承诺一

  下一个承诺

  SetTimeout 0 //说明问题1。没有上面的nextTick和promise,setTimeout和setImmediate的顺序就不一定了。如果有,必须先从0开始。

  //可以看到,在执行一个队列之前,检查并执行nextTick和promise微队列。

  setTimeout中的nextTick

  设置超时1

  setTimeout1中的nextTick

  setTimeout1中的承诺

  setImmediate

  setImmediate中的nextTick

  投票1

  轮询中的next tick=====

  设置超时100

  setTimeout100中的nextTick

  setTimeout100中的承诺

  setTimeout 100中的setImmediate

  setTimeout 100中的setImmediate中的nextTick

  设置超时100 - 0

  setTimeout 100 - 0中的nextTick

  */以上代码执行多次,顺序不变。setTimeout和setImmediate的顺序都没有改变。

  执行顺序和具体原因如下:

  全局:主线程同步任务,带头执行没问题。

  NextTick 1:在执行异步宏任务之前,清除异步微任务。nextTick优先级高,所以先行一步。

  NextTick 2:执行完上面的代码,另一个NextTick微任务,马上带头执行。

  承诺1:在执行异步宏任务之前,清除异步微任务。Promise的优先级低,所以在nextTick完成后会立即执行。

  promise中的NextTick:清空Promise队列的过程中遇到next tick微任务时,立即执行并清空。

  SetTimeout 0:解释第一个问题。上面没有nextTick和promise,只有SetTimeout0和setImmediate,它们的执行顺序不一定相同。有了之后,必须从0开始。可以看出,在执行一个宏队列之前,依次检查并执行nextTick和promise微队列。当所有微队列执行完毕,设置timeout (0)的时机成熟时,就会执行。

  setTimeout中的NextTick:执行完上面的代码,另一个nextTick微任务,立即率先执行【这个回调函数中的微任务,我不确定是在同步任务之后立即执行的;或者将它们放入微任务队列,然后在执行下一个宏任务之前清空它们。但是顺序看起来和立即执行它们是一样的。但是,我更喜欢后者:在微任务队列中等待,在下一个宏任务执行之前清空。】

  setTimeout 1:因为执行微任务需要时间,所以判断timers中两个0和1的SetTimeout定时器此时已经结束,所以两个SetTimeout回调都已经加入队列并执行。

  setTimeout1中的NextTick:执行完上面的代码后,另一个nextTick微任务,立即率先执行【可能在下一个宏任务之前清除微任务】

  setTimeout1中的Promise:执行完上面的代码,另一个promise微任务,立即跟随【可能是微任务在下一个宏任务之前被清零】

  SetImmediate:还没到回调轮询队列的时候。首先转到检查队列,清空队列,然后立即执行setImmediate:poll back。

  setImmediate中的NextTick:执行完上面的代码后,另一个next tick微任务,立即率先执行它【可能在下一个宏任务之前清除微任务】

  轮询1:轮询队列实际上是成熟的,回调触发器,同步任务执行。

  poll中的NextTick:执行完上述代码后,另一个next tick微任务,立即率先执行[可能在下一个宏任务之前清除微任务]

  SetTimeout 100:定时任务到达时间,并执行回调。在回调中,nextTick和Promise被推送到微任务中,setImmediate的回调被推送到宏任务的check中。并且还启动了定时器线程,增加了定时器下一轮回调的可能性。

  setTimeout100中的NextTick:宏任务下行,定时器回调中新增加的微任务先执行——next tick【这里就能确定了,是下一个宏任务前清空微任务的流程】

  setTimeout100中的Promise:立即执行定时器回调中新添加的微任务——Promise【清空完nextTick清空Promise的顺序】

  setTimeout 100中的SetImmediate:这次在setTimeout(0)之前执行setImmediate的原因是:进程从timers回到检查队列,已经有set immediate的回调,所以立即执行。

  settimeout100中set immediate中的NextTick:执行完上述代码后,另一个next tick微任务,下一个宏任务前率先清空微任务

  SetTimeout 100-0:轮询再次返回计时器,执行100-0回调。

  setTimeout 100-0中的NextTick:执行完上述代码后,另一个nextTick微任务,下一个宏任务前率先清空微任务。

  

扩展:为什么有了setImmediate还要有nextTick和Promise?

  最初设计的时候,setImmediate充当的是一个微队列(虽然他不是)。设计者希望在执行完poll之后立即执行setImmediate(当然现在也是这种情况)。所以名字叫立即,就是立即的意思。

  但接下来的问题是,poll中可能有n个任务是连续执行的,在执行过程中不可能执行setImmediate。因为轮询队列不会停止,所以进程不会停止。

  于是真正的微队列概念nextTick出现了。但此时immediate的名字被占用了,所以名字叫nextTick。在循环期间,在执行任何队列之前,检查它是否为空。其次,承诺。

  

面试题

  最后,检验学习成果的面试题来了。

  异步函数async1() {

  console.log(“异步启动”);

  await async 2();

  console . log(“async end”);

  }

  异步函数async2(){

  console . log(“async 2”);

  }

  console.log(脚本开始);

  setTimeout(()={

  console . log( settime out 0 );

  }, 0)

  setTimeout(()={

  console . log( settime out 3 );

  }, 3)

  setImmediate(()={

  console . log( set immediate );

  })

  process.nextTick(()={

  console . log( next tick );

  })

  async 1();

  新承诺((res)={

  console . log( promise 1 );

  RES();

  console . log( promise 2 );

  }).然后(()={

  console.log(无极3 );

  });

  console.log(脚本结束);

  //答案如下

  //-

  //-

  //-

  //-

  //-

  //-

  //-

  //-

  //-

  //-

  //-

  //-

  /**

  脚本开始

  异步启动

  异步2

  承诺1

  承诺2

  脚本结束

  下一滴答

  异步端

  承诺3

  //最后三个的运行顺序,就是验证你电脑运算速度的时候了。

  最佳速度(执行上述同步代码微任务定时器操作用时不到0ms):

  setImmediate

  setTimeout 0

  设置超时3

  中速(执行上述同步代码微任务定时器操作用了0~3ms以上):

  setTimeout 0

  setImmediate

  设置超时3

  速度差(执行上述同步代码微任务定时器操作用了3ms以上):

  setTimeout 0

  设置超时3

  setImmediate

  */

思维脑图 - Node生命周期核心阶段

  有关编程的更多信息,请访问:编程视频!以上是图文结合,帮助你理解Nodejs中事件循环的细节。请多关注我们的其他相关文章!

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

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