Maison > Article > interface Web > Analyser le mécanisme de boucle d'événements de Node.js
Le contenu de cet article concerne l'analyse du mécanisme de boucle d'événements de Node.js. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.
Dans l'article sur le navigateur, le mécanisme de boucle d'événements et certains concepts associés ont été présentés en détail, mais c'est principalement pour la recherche côté navigateur. Est-ce la même chose pour l'environnement Node ? Jetons d'abord un coup d'œil à une démo :
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0)
Compilez-la et exécutez-la à l'œil nu. Le résultat dans le navigateur est le suivant. Vous connaissez déjà la vérité, je n'entrerai donc pas dans les détails. ici.
timer1 promise1 timer2 promise2
Ensuite, exécutez-le sous Node, hein. . . Étrange, les résultats d'exécution sont différents de ceux du navigateur~
timer1 timer2 promise1 promise2
L'exemple montre que le mécanisme de boucle d'événements du navigateur et de Node.js est différent, jetons-y un coup d'oeil~
Traitement des événements Node.js
Node.js utilise V8 comme moteur d'analyse js et utilise sa propre libuv conçue pour le traitement des E/S. libuv est une couche d'abstraction multiplateforme basée sur les événements. encapsule certaines fonctionnalités sous-jacentes de différents systèmes d'exploitation et fournit une API unifiée au monde extérieur. Le mécanisme de boucle d'événements y est également implémenté. Référence du code source principal :
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); // timers阶段 uv__run_timers(loop); // I/O callbacks阶段 ran_pending = uv__run_pending(loop); // idle阶段 uv__run_idle(loop); // prepare阶段 uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); // poll阶段 uv__io_poll(loop, timeout); // check阶段 uv__run_check(loop); // close callbacks阶段 uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
Selon l'introduction officielle de Node. js, chaque boucle d'événement contient Il y a 6 étapes, correspondant à l'implémentation dans le code source de libuv, comme le montre la figure ci-dessous
étape timers : Cette étape exécute le rappel de la minuterie (setTimeout, setInterval)
Phase de rappels d'E/S : exécute certaines erreurs d'appel système, telles que les rappels d'erreurs de communication réseau
inactif, phase de préparation : utilisée uniquement en interne par le nœud
phase d'interrogation : obtenez un nouvel événement d'E/S, dans des conditions appropriées, le nœud bloquera ici
phase de vérification : exécutez le rappel de setImmediate()
phase de rappels de fermeture : exécutez la fermeture rappel d'événement du socket
Notre objectif Il suffit de regarder les trois étapes des minuteries, d'interroger et de vérifier, car la plupart des tâches asynchrones du développement quotidien sont traitées dans ces trois étapes.
phase des minuteries
les minuteries sont la première phase de la boucle d'événements, Node Vérifiera s'il y a un minuteur expiré, et si c'est le cas, poussera son rappel dans la file d'attente des tâches du minuteur pour attendre l'exécution. En fait, Node. Il n'y a aucune garantie que le minuteur sera exécuté immédiatement lorsque l'heure prédéfinie est atteinte, car la vérification de l'expiration du minuteur par Node n'est pas nécessairement fiable. Elle sera affectée par d'autres programmes en cours d'exécution sur la machine, ou le thread principal n'est pas inactif. cette fois-là. Par exemple, dans le code suivant, l'ordre d'exécution de setTimeout() et setImmediate() est incertain.
setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') })
Mais si vous les mettez dans un rappel d'E/S, setImmediate() doit être exécuté en premier, car la phase d'interrogation est suivie de la phase de vérification.
Phase d'interrogation
La phase d'interrogation a principalement deux fonctions :
Traitement des événements dans la file d'attente d'interrogation
Lorsqu'un minuteur a expiré, exécuter sa fonction de rappel
la boucle paire exécutera de manière synchrone les rappels dans la file d'attente d'interrogation jusqu'à ce que la file d'attente soit vide ou que les rappels exécutés atteignent la limite supérieure du système (la limite supérieure est inconnue), puis la boucle paire vérifiera si il existe un prédéfini setImmediate(), divisé en deux situations :
S'il existe un prédéfini setImmediate(), la boucle d'événements mettra fin à la phase d'interrogation et entrera dans la phase de vérification, et exécutera la file d'attente des tâches de la vérification phase
S'il n'y a pas de setImmediate() prédéfini, la boucle d'événement se bloquera et attendra à ce stade
Notez un détail, aucun setImmediate() ne provoquera l'événement La boucle est bloquée dans la phase d'interrogation, le timer précédemment réglé ne pourrait-il donc pas être exécuté ? Ainsi, lors de la phase de sondage, l'événement La boucle aura un mécanisme de vérification pour vérifier si la file d'attente du minuteur est vide. Si la file d'attente du minuteur n'est pas vide, l'événement. La boucle démarre le prochain tour de boucle d'événements, c'est-à-dire entre à nouveau dans la phase de minuterie.
phase de vérification
Le rappel de setImmediate() sera ajouté à la file d'attente de vérification. À partir du diagramme de phase de la boucle d'événements, nous pouvons savoir que l'ordre d'exécution de la phase de vérification est après l'interrogation. phase.
Résumé
Chaque étape de la boucle d'événements a une file d'attente de tâches
Lorsque la boucle d'événements atteint une certaine étape, la file d'attente de tâches de cette étape sera exécutée jusqu'à ce que la file d'attente est effacé. Ou lorsque le rappel exécuté atteint la limite du système, il passera à l'étape suivante
Lorsque toutes les étapes sont exécutées en séquence une fois, la boucle d'événements est dite avoir terminé un tick
C'est logique, mais sans la démo, je ne comprends toujours pas complètement. Je suis anxieux, maintenant !
const fs = require('fs')fs.readFile('test.txt', () => { console.log('readFile') setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
Il ne devrait y avoir aucun doute sur le résultat de l'exécution
readFile immediate timeout
Différences entre Node.js et la boucle d'événement du navigateur
Revue de l'article précédent, dans le navigateur environnement , la file d'attente des tâches de la microtâche est exécutée après l'exécution de chaque macrotâche.
Dans Node.js, les microtâches seront exécutées entre différentes étapes de la boucle d'événements, c'est-à-dire qu'après l'exécution d'une étape, les tâches de la file d'attente des microtâches seront exécutées .
Revue de la démo
En revue la démo au début de l'article, le script global (main()) est exécuté, et les deux minuteries sont successivement placées dans la file d'attente des minuteries, main() est exécutée, la pile d'appels est inactive et la file d'attente des tâches commence à s'exécuter
;首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。
对比浏览器端的处理过程:
process.nextTick() VS setImmediate()
In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate()
来自官方文档有意思的一句话,从语义角度看,setImmediate() 应该比 process.nextTick() 先执行才对,而事实相反,命名是历史原因也很难再变。
process.nextTick() 会在各个事件阶段之间执行,一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用 process.nextTick(),会导致出现I/O starving(饥饿)的问题,比如下面例子的readFile已经完成,但它的回调一直无法执行:
const fs = require('fs')const starttime = Date.now()let endtime fs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime)})let index = 0function handler () { if (index++ >= 1000) return console.log(`nextTick ${index}`) process.nextTick(handler) // console.log(`setImmediate ${index}`) // setImmediate(handler)}handler()
process.nextTick()的运行结果:
nextTick 1 nextTick 2 ...... nextTick 999 nextTick 1000 finish reading time: 170
替换成setImmediate(),运行结果:
setImmediate 1 setImmediate 2 finish reading time: 80 ...... setImmediate 999 setImmediate 1000
这是因为嵌套调用的 setImmediate() 回调,被排到了下一次event loop才执行,所以不会出现阻塞。
总结
1、Node.js 的事件循环分为6个阶段
2、浏览器和Node 环境下,microtask 任务队列的执行时机不同
Node.js中,microtask 在事件循环的各个阶段之间执行
浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
3、递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()
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!