分析Node.js中的event-loop机制
导读:本文共4820字符,通常情况下阅读需要16分钟。同时您也可以点击右侧朗读,来听本文内容。按键盘←(左) →(右) 方向键可以翻页。
摘要: libuv在学习event-loop之前,先了解下node的libuv。libuv负责不同操作系统上的不同I/O模型的实现,并且把不同的实现抽象为能应用与第三方应用程序的API。问题在正式学习event-loop前,先思考一个问题setTimeout(()=>{console.log("timer1");Promise.resolv... ...
目录
(为您整理了一些要点),点击可以直达。libuv
在学习event-loop之前,先了解下node的libuv。libuv负责不同操作系统上的不同I/O模型的实现,并且把不同的实现抽象为能应用与第三方应用程序的API。
问题
在正式学习event-loop前,先思考一个问题
setTimeout(()=>{console.log("timer1");Promise.resolve().then(()=>{console.log("promise1");});},0);setTimeout(()=>{console.log("timer2");Promise.resolve().then(()=>{console.log("promise2");});},0);
这段代码在浏览器中运行的结果是怎样的?
在node中运行的结果又是怎样的呢?
在node8.6之前:
node8.6之后:
为什么会有这样的结果,我们稍后分析!
nodeJs 中的event-loop
首先,来看一张图:
在图中可以看到6个阶段,分别是:timers,pending callbacks,idle/prepare,poll,check,close callbacks。
timers阶段:主要执行setTimeOut,setInterval的回调
pending callbacks阶段:执行一些系统调用的错误,比如说网络通信的错误回调
idle/prepare阶段:只在系统内部使用(这个阶段我们控制干涉不了)
poll阶段:获取新的I/O事件,比如获取一个读取文件的I/O回调。在适合的情况下,nodejs将阻塞在这个阶段
check阶段:执行 setImmediate的回调
比如执行sokect的destory,close事件回调
每一个阶段都遵循一个FIFO(先入先出)的规则来执行任务队列里面的任务。在这六个阶段中,我们着重需要关注的是timers,poll,check阶段。我们日常开发中的绝大部分异步任务都是在这三个阶段处理的。
timers
我们先来说一说timers阶段。
timers是事件循环的第一个阶段,nodejs会去检查有没有已经过期了的timer,如果有,就将它的回调放入队列中。但是nodejs并不能保证timer在预设事件到了就会立即执行回调,这是因为nodejs对timer的过期检查不一定靠谱,它会受机器上其他运行程序的影响,或者是会遇到当前主线程不空闲的情况。
对于这里的不确定性,官网上举了一个例子:
先声明一个setTimeOut,然后外部读取一个文件,当读取文件操作超过定时器的时间,这样一来读文件操作就会把定时器的回调延后,这就是前面说的主线程不空闲的情况。
poll
poll阶段主要是执行两件事情:
1、处理poll阶段的任务队列
2、当有了已经超时的timer执行它的回调函数
在上图中,我们还可以看到:在poll阶段执行完poll任务队列的任务之后,会去检查有无预设的setImmediate,如果有,则进入check阶段,如果没有,则nodejs将会阻塞在这里。
这里我们就会有一个疑问了,如果阻塞在poll阶段,那我们设置的timer岂不是执行不了了吗?
其实当event-loop阻塞在poll阶段时,nodejs会有一个检查机制,它会去检查timers队列是否为空,如果不为空,则重新进入timers阶段。
check
check阶段主要时执行setImmediate的回调函数。
小总结
event-loop的每个阶段都有一个队列,当event-loop达到某个阶段之后,将执行这个阶段的任务队列,直到队列清空或者达到系统规定的最大回调限制之后,才会进入下一个阶段。当所有阶段都执行完成一次之后,称event-loop完成一个tick。
案例
上面我们说完了event-loop的理论部分,但是光有理论我们也还是不能很清晰的理解event-loop。下面我们就根据几个demo来更加深入的理解下event-loop!
demo1
constfs=require('fs')fs.readFile('test.txt',()=>{console.log('readFile')setTimeout(()=>{console.log('settimeout');},0)setImmediate(()=>{console.log('setImmediate')})})
执行结果:
可见执行结果跟我们前面的分析时一致的!
demo2
constfs=require("fs");constEventEmitter=require("events").EventEmitter;letpos=0;constmessenger=newEventEmitter();messenger.on("message",function(msg){console.log(++pos+"message:"+msg);//});console.log(++pos+"first");//process.nextTick(function(){console.log(++pos+"nextTick");//});messenger.emit("message","hello!");fs.stat(__filename,function(){console.log(++pos+"stat");//});setTimeout(function(){console.log(++pos+"quicktimer");//},0);setTimeout(function(){console.log(++pos+"longtimer");//},30);setImmediate(function(){console.log(++pos+"immediate");//});console.log(++pos+"last");//
结果:
了解下浏览器和node的event-loop差异在什么地方
在node 8.6 之前:
浏览器中的微任务队列会在每个宏任务执行完成之后执行,而node中的微任务会在事件循环的各个阶段之间执行,即每个阶段执行完成之后会去执行微任务队列。
在8.6之后:
浏览器和node中微任务的执行是一致的!
所以,在文章开头,我们提出的思考的问题就有了结果。
关于 process.nextTick()和setImmediate
process.nextTick()
语法:process.nextTick(callback,agrs)
执行时机:
这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。递归的调用process.nextTick()
会导致I/O starving,官方推荐使用setImmediate()
关于starving现象的说明:
constfs=require("fs");fs.readFile("test.txt",(err,msg)=>{console.log("readFile");});letindex=0;functionhandler(){if(index>=30)return;index++;console.log("nextTick"+index);process.nextTick(handler);}handler();
运行结果:
可以看到,等到nextTick函数呗执行30次之后,读取文件的回调才被执行!这样的现象被称为 I/O 饥饿。
当我们把 process.nextTick 换为 setImmediate
constfs=require("fs");fs.readFile("test.txt",(err,msg)=>{console.log("readFile");});letindex=0;functionhandler(){if(index>=30)return;index++;console.log("nextTick"+index);setImmediate(handler);}handler();
结果:
造成这两种差异的原因是,嵌套调用的setImmediate的回调被排到了下一次event-loop中去!
event-loop核心思维导图
</div> <div class="zixun-tj-product adv-bottom"></div> </div> </div> <div class="prve-next-news">
分析Node.js中的event-loop机制的详细内容,希望对您有所帮助,信息来源于网络。