Node Eventloop
Posted Web News
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Node Eventloop相关的知识,希望对你有一定的参考价值。
浏览器
关于微任务和宏任务在浏览器的执行顺序是这样的:
执行一只task(宏任务)
执行完micro-task队列 (微任务)
如此循环往复下去
常见的 task(宏任务) 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。
常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。
Node
这个图是整个 Node.js 的运行原理,从左到右,从上到下,Node.js 被分为了四层,分别是 应用层
、V8引擎层
、Node API层
和 LIBUV层
。
应用层: 即 javascript 交互层,常见的就是 Node.js 的模块,比如 http,fs
V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
NodeAPI层: 为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互 。
LIBUV层:是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心 。
当Node.js启动时会初始化event loop
, 每一个event loop
都会包含按如下顺序六个循环阶段:
Node的事件循环是libuv实现的,引用一张官网的图:
大体的task(宏任务)执行顺序是这样的:
timers定时器:本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。
pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。
idle, prepare:仅系统内部使用。
poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
check 检测:setImmediate() 回调函数在这里执行。
close callbacks 关闭的回调函数:一些准备关闭的回调函数,如:socket.on('close', ...)。
图1
将事件循环的 Pending、Idle/Prepare 和 Close 阶段涂成灰色,因为这些是 Node 在内部使用的阶段。Node 开发者编写的代码仅以微任务形式在主线、计时器(Timers) 阶段、轮询(Poll) 阶段和 查询(Check) 阶段中运行。
回调队列
每个阶段都会有一个在该阶段执行的 FIFO 回调队列。在图 1 中,为了节省空间,我没有展示每个阶段的回调队列,所以您可以想象一下每个阶段中的队列。
队列中的回调会一直运行,直到队列变空或达到系统相关限制。Node 文档没有明确说明这一点。
微任务
微任务紧跟在主线之后和事件循环的每个阶段之后执行。
在 Node 领域,微任务是来自以下位置的回调:
process.nextTick()
已解决或被拒绝的 Promise 的
then()
处理函数(handler)
在主线和事件循环的每个阶段完成后,会立刻运行微任务回调。
process.nextTick
不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感觉.setImmediate
的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行.
Timers 阶段
计时器分为两类:
Immediate
Timeout
当不再有过期的计时器回调需要运行时,事件循环就会运行所有微任务。运行微任务后,事件循环就会进入 Pending 阶段。
Pending 阶段
一些系统级回调将会在此阶段执行。您实际上不必担心此阶段(老实说,关于此阶段的信息并不多),但是我希望您知道它的存在。
Idle 和Prepare 阶段
显然,此阶段“仅供内部使用”,所以我不会介绍它。同样地,只需要知道它的存在就行了。
Poll 阶段
通常,如果轮询队列为空,则会阻塞并等待任何正在执行的 I/O 操作完成,然后立刻执行这些操作的回调。但是,如果调度了计时器,则 Poll 阶段将会结束。在必要时运行微任务,然后事件循环会进入 Check 阶段。
轮询 阶段有两个重要的功能:
计算应该阻塞和轮询 I/O 的时间。
然后,处理 轮询 队列里的事件。
当事件循环进入 轮询 阶段且 没有被调度的计时器时 ,将发生以下两种情况之一:
如果 轮询 队列 不是空的 ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。
如果 轮询 队列 是空的 ,还有两件事发生:
如果脚本被
setImmediate()
调度,则事件循环将结束 轮询 阶段,并继续 检查 阶段以执行那些被调度的脚本。如果脚本 未被
setImmediate()
调度,则事件循环将等待回调被添加到队列中,然后立即执行。
一旦 轮询 队列为空,事件循环将检查 _已达到时间阈值的计时器_。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。
Check 阶段
此阶段是一种“后 I/O”阶段,只有 setImmediate()
回调会在该阶段中执行。这使您能够在 Poll 阶段变得空闲时立即执行一些代码。
Check 阶段的回调队列为空后,会运行所有微任务,然后事件循环将进入 Close 阶段。
这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。
Close 阶段
如果某个套接字(socket)或句柄(handle)突然关闭(例如,如果调用了一个套接字的 socket.destroy()
方法),则会执行此阶段,这种情况下会触发其“close”事件。
因为这种情况不太可能发生,所以我在详细讨论时会省略此阶段(我只会讨论您可能在其中执行回调的阶段)。
总结:
微任务和宏任务在Node的执行顺序
Node 10以前:
执行完一个阶段的所有任务
执行完nextTick队列里面的内容
然后执行完微任务队列的内容
Node 11以后:
和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。
示例题:
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout0')
},0)
setTimeout(function(){
console.log('setTimeout3')
},3)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})
console.log('script end'
依次打印:
script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3
setTimeout0
setImmediate
setTimeout3
你答对了吗
以上是关于Node Eventloop的主要内容,如果未能解决你的问题,请参考以下文章