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实现的,引用一张官网的图:

Node Eventloop


大体的task(宏任务)执行顺序是这样的:

  • timers定时器:本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。

  • pending callbacks待定回调:执行延迟到下一个循环迭代的 I/O 回调。

  • idle, prepare:仅系统内部使用。

  • poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。

  • check 检测:setImmediate() 回调函数在这里执行。

  • close callbacks 关闭的回调函数:一些准备关闭的回调函数,如:socket.on('close', ...)。


Node Eventloop

图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 阶段。

轮询 阶段有两个重要的功能:

  1. 计算应该阻塞和轮询 I/O 的时间。

  2. 然后,处理 轮询 队列里的事件。

当事件循环进入 轮询 阶段且 没有被调度的计时器时 ,将发生以下两种情况之一:

  • 如果 轮询 队列 不是空的 ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。

  • 如果 轮询 队列 是空的 ,还有两件事发生:

    • 如果脚本被 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 startasync1 startasync2promise1promise2script endnextTickasync1 endpromise3setTimeout0setImmediatesetTimeout3

你答对了吗

以上是关于Node Eventloop的主要内容,如果未能解决你的问题,请参考以下文章

Node.js源码解读-EventLoop

周一硬核干货:通过Node.js的源码彻底理解EventLoop

EventLoop在浏览器和node中的区别

跟着大佬走——node中的eventloop

今日分享Node 核心和 Node eventLoop

node和web的EventLoop