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的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。