Maison >interface Web >js tutoriel >Une compréhension approfondie de la boucle d'événements dans Node

Une compréhension approfondie de la boucle d'événements dans Node

青灯夜游
青灯夜游avant
2021-12-31 19:18:551564parcourir

Node.js est un langage monothread qui gère les opérations d'E/S non bloquantes via une boucle d'événements. L'article suivant vous donnera une compréhension détaillée de la boucle d'événements dans Node. J'espère qu'il vous sera utile !

Une compréhension approfondie de la boucle d'événements dans Node

Node.js En tant que runtime côté serveur pour JavaScript, il s'occupe principalement du réseau et des fichiers, sans la phase de rendu de la boucle d'événements dans le navigateur.

Il existe une spécification HTML dans le navigateur pour définir le modèle de traitement de la boucle d'événements, qui est ensuite implémenté par divers fabricants de navigateurs. La définition et l'implémentation de la boucle d'événements dans Node.js proviennent de Libuv.

Libuv est conçu autour du modèle d'E/S asynchrones piloté par événements et a été initialement écrit pour Node.js, fournissant une bibliothèque de support multiplateforme. La figure ci-dessous montre ses composants. Les E/S réseau sont la partie liée au traitement réseau. Il y a également les opérations de fichiers et DNS sur le côté droit, epoll, kqueue, les ports d'événements et IOCP sont les implémentations des différents sous-jacents. systèmes d'exploitation.

Une compréhension approfondie de la boucle dévénements dans Node

Les six étapes de la boucle d'événements

Lorsque Node.js démarre, il initialise la boucle d'événements, traite le script fourni, le code synchrone est poussé dans la pile pour une exécution directe et les tâches asynchrones (réseau requêtes, opérations sur les fichiers, timing Après avoir appelé l'API et passé la fonction de rappel, l'opération sera transférée en arrière-plan pour être traitée par le noyau du système. La plupart des noyaux actuels sont multithreads. Lorsqu'une des opérations est terminée, le noyau demande à Node.js d'ajouter la fonction de rappel à la file d'attente d'interrogation et d'attendre l'opportunité de s'exécuter.

Le côté gauche de l'image ci-dessous est la description du processus de boucle d'événement sur le site officiel de Node.js, et le côté droit est la description de Node.js sur le site officiel de Libuv. Ce sont tous deux des introductions à la boucle d'événement. .Tout le monde ne peut pas consulter le code source.Les deux documents sont généralement des documents de référence plus directs pour en savoir plus sur la boucle d'événements.L'introduction sur le site officiel de Node.js est assez détaillée et peut être utilisée comme référence pour l'apprentissage.

Une compréhension approfondie de la boucle dévénements dans Node

La boucle d'événements affichée sur le site officiel de Node.js à gauche est divisée en 6 étapes. Chaque étape a une file d'attente FIFO (premier entré, premier sorti) pour exécuter la fonction de rappel. L'ordre de priorité d'exécution entre. ces étapes sont encore claires.

Le côté droit le décrit plus en détail. Avant d'itérer la boucle d'événements, déterminez d'abord si la boucle est dans un état actif (il y a des E/S asynchrones en attente, des minuteries, etc.). démarrez l'itération, sinon la boucle se terminera immédiatement.

Chaque étape sera discutée séparément ci-dessous.

timers (phase de minuterie)

Tout d'abord, la boucle d'événements entre dans la phase de minuterie, qui contient deux API setTimeout(cb, ms) et setInterval(cb, ms). La première n'est exécutée qu'une seule fois et la seconde est répétée. mettre en œuvre.

Cette étape vérifie s'il existe une fonction de minuterie expirée. Si c'est le cas, la fonction de rappel de la minuterie expirée est exécutée. Tout comme dans le navigateur, le temps de retard transmis par la fonction de minuterie est toujours plus tard que prévu. affecté par le système d’exploitation ou d’autres fonctions de rappel en cours d’exécution.

Par exemple, dans l'exemple suivant, nous définissons une fonction de minuterie et nous nous attendons à ce qu'elle s'exécute après 1 000 millisecondes.

const now = Date.now();
setTimeout(function timer1(){
  log(`delay ${Date.now() - now} ms`);
}, 1000);
setTimeout(function timer2(){
 log(`delay ${Date.now() - now} ms`);
}, 5000);
someOperation();

function someOperation() {
  // sync operation...
  while (Date.now() - now < 3000) {}
}

Après avoir appelé la fonction asynchrone setTimeout, le programme exécute ensuite la fonction someOperation(). Certaines opérations fastidieuses au milieu consomment environ 3 000 ms. Après avoir terminé ces opérations synchrones, il entre dans une boucle d'événements et vérifie d'abord si la phase de minuterie. a expiré. Les scripts de minuterie sont stockés dans la mémoire tas par ordre croissant de délai. Tout d'abord, la fonction de minuterie avec le délai d'expiration le plus court est supprimée pour vérification. Si nowTime - timerTaskRegisterTime > retiré et exécuté Sinon, continuez à vérifier Lors de la vérification Après avoir atteint une fonction de minuterie qui n'a pas expiré ou atteint le nombre maximum de dépendances du système, passez à l'étape suivante.

Dans notre exemple, supposons que l'heure actuelle après l'exécution de la fonction someOperation() est T + 3000 :

Vérifiez la fonction timer1, l'heure actuelle est T + 3000 - T > après l'exécution de la fonction de rappel, continuez la vérification.

Vérifiez la fonction timer2, l'heure actuelle est T + 3000 - T

rappels en attente

Une fois la phase de minuterie terminée, la boucle d'événements entre dans la phase de rappels en attente, où les rappels d'E/S restants du tour précédent de la boucle d'événements sont exécutés. Selon la documentation Libuv : Dans la plupart des cas, tous les rappels d'E/S sont appelés immédiatement après l'interrogation des E/S, cependant, dans certains cas, l'appel de ces rappels est différé jusqu'à la prochaine itération de boucle. Après l’avoir écouté, cela ressemble davantage à un vestige de l’étape précédente.

idle, prepare

idle, prepare 阶段是给系统内部使用,idle 这个名字很迷惑,尽管叫空闲,但是在每次的事件循环中都会被调用,当它们处于活动状态时。这一块的资料介绍也不是很多。略...

poll

poll 是一个重要的阶段,这里有一个概念观察者,有文件 I/O 观察者,网络 I/O 观察者等,它会观察是否有新的请求进入,包含读取文件等待响应,等待新的 socket 请求,这个阶段在某些情况下是会阻塞的。

阻塞 I/O 超时时间

在阻塞 I/O 之前,要计算它应该阻塞多长时间,参考 Libuv 文档上的一些描述,以下这些是它计算超时时间的规则:

如果循环使用 UV_RUN_NOWAIT 标志运行、超时为 0。

如果循环将要停止(uv_stop() 被调用),超时为 0。

如果没有活动的 handlers 或 request,超时为 0。

如果有任何 idle handlers 处于活动状态,超时为 0。

如果有任何待关闭的 handlers,超时为 0。

如果以上情况都没有,则采用最近定时器的超时时间,或者如果没有活动的定时器,则超时时间为无穷大,poll 阶段会一直阻塞下去。

示例一

很简单的一段代码,我们启动一个 Server,现在事件循环的其它阶段没有要处理的任务,它会在这里等待下去,直到有新的请求进来。

const http = require(&#39;http&#39;);
const server = http.createServer();
server.on(&#39;request&#39;, req => {
  console.log(req.url);
})
server.listen(3000);

示例二

结合阶段一的定时器,在看个示例,首先启动 app.js 做为服务端,模拟延迟 3000ms 响应,这个只是为了配合测试。再运行 client.js 看下事件循环的执行过程:

首先程序调用了一个在 1000ms 后超时的定时器。

之后调用异步函数 someAsyncOperation() 从网络读取数据,我们假设这个异步网路读取需要 3000ms。

当事件循环开始时先进入 timer 阶段,发现没有超时的定时器函数,继续向下执行。

期间经过 pending callbacks -> idle,prepare 当进入 poll 阶段,此时的 http.get() 尚未完成,它的队列为空,参考上面 poll 阻塞超时时间规则,事件循环机制会检查最快到达阀值的计时器,而不是一直在这里等待下去。

当大约过了 1000ms 后,进入下一次事件循环进入定时器,执行到期的定时器回调函数,我们会看到日志 setTimeout run after 1003 ms。

在定时器阶段结束之后,会再次进入 poll 阶段,继续等待。

// client.js
const now = Date.now();
setTimeout(() => log(`setTimeout run after ${Date.now() - now} ms`), 1000);
someAsyncOperation();
function someAsyncOperation() {
  http.get(&#39;http://localhost:3000/api/news&#39;, () => {
    log(`fetch data success after ${Date.now() - now} ms`);
  });
}

// app.js
const http = require(&#39;http&#39;);
http.createServer((req, res) => {
  setTimeout(() => { res.end(&#39;OK!&#39;) }, 3000);
}).listen(3000);

当 poll 阶段队列为空时,并且脚本被 setImmediate() 调度过,此时,事件循环也会结束 poll 阶段,进入下一个阶段 check。

check

check 阶段在 poll 阶段之后运行,这个阶段包含一个 API setImmediate(cb) 如果有被 setImmediate 触发的回调函数,就取出执行,直到队列为空或达到系统的最大限制。

setTimeout VS setImmediate

拿 setTimeout 和 setImmediate 对比,这是一个常见的例子,基于被调用的时机和定时器可能会受到计算机上其它正在运行的应用程序影响,它们的输出顺序,不总是固定的。

setTimeout(() => log(&#39;setTimeout&#39;));
setImmediate(() => log(&#39;setImmediate&#39;));

// 第一次运行
setTimeout
setImmediate

// 第二次运行
setImmediate
setTimeout

setTimeout VS setImmediate VS fs.readFile

但是一旦把这两个函数放入一个 I/O 循环内调用,setImmediate 将总是会被优先调用。因为 setImmediate 属于 check 阶段,在事件循环中总是在 poll 阶段结束后运行,这个顺序是确定的。

fs.readFile(__filename, () => {
  setTimeout(() => log(&#39;setTimeout&#39;));
  setImmediate(() => log(&#39;setImmediate&#39;));
})

close callbacks

在 Libuv 中,如果调用关闭句柄 uv_close(),它将调用关闭回调,也就是事件循环的最后一个阶段 close callbacks。

这个阶段的工作更像是做一些清理工作,例如,当调用 socket.destroy(),'close' 事件将在这个阶段发出,事件循环在执行完这个阶段队列里的回调函数后,检查循环是否还 alive,如果为 no 退出,否则继续下一次新的事件循环。

包含 Microtask 的事件循环流程图

在浏览器的事件循环中,把任务划分为 Task、Microtask,在 Node.js 中是按照阶段划分的,上面我们介绍了 Node.js 事件循环的 6 个阶段,给用户使用的主要是 timer、poll、check、close callback 四个阶段,剩下两个由系统内部调度。这些阶段所产生的任务,我们可以看做 Task 任务源,也就是常说的 “Macrotask 宏任务”。

通常我们在谈论一个事件循环时还会包含 Microtask,Node.js 里的微任务有 Promise、还有一个也许很少关注的函数 queueMicrotask,它是在 Node.js v11.0.0 之后被实现的,参见 PR/22951。

Node.js 中的事件循环在每一个阶段执行后,都会检查微任务队列中是否有待执行的任务。

Une compréhension approfondie de la boucle dévénements dans Node

Node.js 11.x 前后差异

Node.js 在 v11.x 前后,每个阶段如果即存在可执行的 Task 又存在 Microtask 时,会有一些差异,先看一段代码:

setImmediate(() => {
  log(&#39;setImmediate1&#39;);
  Promise.resolve(&#39;Promise microtask 1&#39;)
    .then(log);
});
setImmediate(() => {
  log(&#39;setImmediate2&#39;);
  Promise.resolve(&#39;Promise microtask 2&#39;)
    .then(log);
});

在 Node.js v11.x 之前,当前阶段如果存在多个可执行的 Task,先执行完毕,再开始执行微任务。基于 v10.22.1 版本运行结果如下:

setImmediate1
setImmediate2
Promise microtask 1
Promise microtask 2

在 Node.js v11.x 之后,当前阶段如果存在多个可执行的 Task,先取出一个 Task 执行,并清空对应的微任务队列,再次取出下一个可执行的任务,继续执行。基于 v14.15.0 版本运行结果如下:

setImmediate1
Promise microtask 1
setImmediate2
Promise microtask 2

在 Node.js v11.x 之前的这个执行顺序问题,被认为是一个应该要修复的 Bug 在 v11.x 之后并修改了它的执行时机,和浏览器保持了一致,详细参见 issues/22257 讨论。

特别的 process.nextTick()

Node.js 中还有一个异步函数 process.nextTick(),从技术上讲它不是事件循环的一部分,它在当前操作完成后处理。如果出现递归的 process.nextTick() 调用,这将会很糟糕,它会阻断事件循环。

如下例所示,展示了一个 process.nextTick() 递归调用示例,目前事件循环位于 I/O 循环内,当同步代码执行完成后 process.nextTick() 会被立即执行,它会陷入无限循环中,与同步的递归不同的是,它不会触碰 v8 最大调用堆栈限制。但是会破坏事件循环调度,setTimeout 将永远得不到执行。

fs.readFile(__filename, () => {
  process.nextTick(() => {
    log(&#39;nextTick&#39;);
    run();
    function run() {
      process.nextTick(() => run());
    }
  });
  log(&#39;sync run&#39;);
  setTimeout(() => log(&#39;setTimeout&#39;));
});

// 输出
sync run
nextTick

将 process.nextTick 改为 setImmediate 虽然是递归的,但它不会影响事件循环调度,setTimeout 在下一次事件循环中被执行。

fs.readFile(__filename, () => {
  process.nextTick(() => {
    log(&#39;nextTick&#39;);
    run();
    function run() {
      setImmediate(() => run());
    }
  });
  log(&#39;sync run&#39;);
  setTimeout(() => log(&#39;setTimeout&#39;));
});

// 输出
sync run
nextTick
setTimeout

process.nextTick 是立即执行,setImmediate 是在下一次事件循环的 check 阶段执行。但是,它们的名字着实让人费解,也许会想这两个名字交换下比较好,但它属于遗留问题,也不太可能会改变,因为这会破坏 NPM 上大部分的软件包。

在 Node.js 的文档中也建议开发者尽可能的使用 setImmediate(),也更容易理解。

更多node相关知识,请访问:nodejs 教程!!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer