Guide Node.js, partie 6: boucle d'événement, pile d'appels, minuteurs

Aujourd'hui, dans la sixiÚme partie de la traduction du manuel Node.js, nous parlerons de la boucle d'événements, de la pile d'appels, de la fonction process.nextTick() et des temporisateurs. La compréhension de ces mécanismes et d'autres mécanismes Node.js est l'une des pierres angulaires du développement d'applications réussi pour cette plate-forme.




Boucle d'événement


Si vous voulez comprendre comment le code JavaScript est exécuté, la boucle d'événement est l'un des concepts les plus importants que vous devez comprendre. Ici, nous allons parler du fonctionnement de JavaScript en mode monothread et de la gestion des fonctions asynchrones.

Je dĂ©veloppe JavaScript depuis de nombreuses annĂ©es, mais je ne peux pas dire que j'ai complĂštement compris comment tout fonctionne, pour ainsi dire, "sous le capot". Le programmeur peut ne pas ĂȘtre au courant des subtilitĂ©s du dispositif des sous-systĂšmes internes de l'environnement dans lequel il travaille. Mais il est gĂ©nĂ©ralement utile d'avoir au moins une idĂ©e gĂ©nĂ©rale de ces choses.

Le code JavaScript que vous Ă©crivez s'exĂ©cute en mode monothread. À un certain moment, une seule action est exĂ©cutĂ©e. Cette limitation est en fait trĂšs utile. Cela simplifie considĂ©rablement le fonctionnement des programmes, Ă©liminant ainsi la nĂ©cessitĂ© pour les programmeurs de rĂ©soudre des problĂšmes spĂ©cifiques aux environnements multithreads.

En fait, un programmeur JS doit faire attention uniquement aux actions que son code effectue exactement et essayer d'éviter les situations qui provoquent le blocage du thread principal. Par exemple - passer des appels réseau en mode synchrone et des cycles sans fin.

En rĂšgle gĂ©nĂ©rale, les navigateurs, dans chaque onglet ouvert, ont leur propre boucle d'Ă©vĂ©nements. Cela vous permet d'exĂ©cuter le code de chaque page dans un environnement isolĂ© et d'Ă©viter les situations oĂč une certaine page, dans le code dont il existe une boucle infinie ou des calculs lourds sont effectuĂ©s, est capable de «suspendre» l'intĂ©gralitĂ© du navigateur. Le navigateur prend en charge le travail de nombreuses boucles d'Ă©vĂ©nements simultanĂ©ment existantes, utilisĂ©es, par exemple, pour traiter les appels vers diverses API. De plus, une boucle d'Ă©vĂ©nement propriĂ©taire est utilisĂ©e pour prendre en charge les travailleurs Web .

La chose la plus importante dont un programmeur JavaScript doit constamment se souvenir est que son code utilise sa propre boucle d'Ă©vĂ©nements, donc le code doit ĂȘtre Ă©crit pour que cette boucle d'Ă©vĂ©nements ne soit pas bloquĂ©e.

Verrou de boucle d'événement


Tout code JavaScript qui prend trop de temps Ă  exĂ©cuter, c'est-Ă -dire un code qui ne prend pas le contrĂŽle de la boucle d'Ă©vĂ©nements trop longtemps, bloque l'exĂ©cution de tout autre code de page. Cela conduit mĂȘme Ă  bloquer le traitement des Ă©vĂ©nements de l'interface utilisateur, ce qui se reflĂšte dans le fait que l'utilisateur ne peut pas interagir avec les Ă©lĂ©ments de la page et travailler normalement avec, par exemple, le dĂ©filement.

Presque tous les mĂ©canismes d'E / S JavaScript de base ne sont pas bloquants. Cela s'applique Ă  la fois au navigateur et Ă  Node.js. Parmi ces mĂ©canismes, par exemple, nous pouvons mentionner les outils pour effectuer des requĂȘtes rĂ©seau utilisĂ©s dans les environnements client et serveur, et les outils pour travailler avec les fichiers Node.js. Il existe des mĂ©thodes synchrones pour effectuer de telles opĂ©rations, mais elles ne sont utilisĂ©es que dans des cas particuliers. C'est pourquoi les rappels traditionnels et les mĂ©canismes plus rĂ©cents - promesses et la construction asynchrone / attendent - sont d'une grande importance dans JavaScript.

Pile d'appels


La pile d'appels JavaScript est basĂ©e sur le principe LIFO (Last In, First Out - Last In, First Out). La boucle d'Ă©vĂ©nements vĂ©rifie constamment la pile d'appels pour voir si elle a une fonction qui doit ĂȘtre exĂ©cutĂ©e. Si, lors de l'exĂ©cution du code, une fonction y est appelĂ©e, des informations la concernant sont ajoutĂ©es Ă  la pile des appels et cette fonction est exĂ©cutĂ©e.

Si mĂȘme avant que vous n'Ă©tiez pas intĂ©ressĂ© par le concept de «pile d'appel», alors si vous avez rencontrĂ© des messages d'erreur qui incluent une trace de pile, vous imaginez dĂ©jĂ  Ă  quoi il ressemble. Ici, par exemple, ressemble Ă  ceci dans un navigateur.


Message d'erreur du navigateur

Le navigateur, lorsqu'une erreur se produit, rend compte de la séquence d'appels aux fonctions, dont les informations sont stockées dans la pile d'appels, ce qui vous permet de trouver la source de l'erreur et de comprendre quels appels à quelles fonctions ont conduit à la situation.

Maintenant que nous avons parlé de la boucle d'événements et de la pile d'appels en termes généraux, considérons un exemple qui illustre l'exécution d'un fragment de code et à quoi ressemble ce processus en termes de boucle d'événements et de pile d'appels.

Boucle d'événement et pile d'appels


Voici le code que nous expérimenterons:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo() 

Si ce code est exécuté, les éléments suivants arriveront à la console:

 foo bar baz 

Un tel rĂ©sultat est tout Ă  fait attendu. A savoir, lorsque ce code est exĂ©cutĂ©, la fonction foo() est d'abord appelĂ©e. À l'intĂ©rieur de cette fonction, nous appelons d'abord la fonction bar() , puis la fonction baz() . Dans le mĂȘme temps, la pile d'appels lors de l'exĂ©cution de ce code subit les modifications illustrĂ©es dans la figure suivante.


Modification de l'Ă©tat de la pile d'appels lors de l'exĂ©cution du code sous enquĂȘte

La boucle d'événements, à chaque itération, vérifie s'il y a quelque chose dans la pile des appels, et si c'est le cas, elle le fait jusqu'à ce que la pile des appels soit vide.


Itérations de boucle d'événement

Mise en file d'attente d'une fonction


L'exemple ci-dessus semble assez ordinaire, il n'a rien de spĂ©cial: JavaScript trouve le code qui doit ĂȘtre exĂ©cutĂ© et l'exĂ©cute dans l'ordre. Nous parlerons de la façon de diffĂ©rer l'exĂ©cution de la fonction jusqu'Ă  ce que la pile d'appels soit effacĂ©e. Pour ce faire, la construction suivante est utilisĂ©e:

 setTimeout(() => {}), 0) 

Il vous permet d'exécuter la fonction passée à la fonction setTimeout() aprÚs l'exécution de toutes les autres fonctions appelées dans le code de programme.

Prenons un exemple:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo() 

Ce que ce code imprime peut sembler inattendu:

 foo baz bar 

Lorsque nous exĂ©cutons cet exemple, la fonction foo() est appelĂ©e en premier. Dans ce document, nous appelons setTimeout() , en passant cette fonction, comme premier argument, bar . En passant 0 comme deuxiĂšme argument, nous informons le systĂšme que cette fonction doit ĂȘtre exĂ©cutĂ©e le plus tĂŽt possible. Ensuite, nous appelons la fonction baz() .

Voici Ă  quoi ressemblera la pile d'appels.


Modification de l'état de la pile d'appels lors de l'exécution du code

Voici l'ordre dans lequel les fonctions de notre programme seront maintenant exécutées.


Itérations de boucle d'événement

Pourquoi cela se passe-t-il ainsi?

File d'attente des événements


Lorsque la fonction setTimeout() est appelée, le navigateur ou la plateforme Node.js démarre une minuterie. Une fois que la minuterie a fonctionné (dans notre cas, cela se produit immédiatement, puisque nous l'avons défini sur 0), la fonction de rappel passée à setTimeout() entre dans la file d'attente d'événements.

La file d'attente des événements, en ce qui concerne le navigateur, comprend les événements déclenchés par l'utilisateur - événements provoqués par des clics de souris sur les éléments de la page, événements qui sont déclenchés lorsque les données sont entrées à partir du clavier. Les gestionnaires d' onload DOM comme onload , les fonctions appelées lors de la réception de réponses aux demandes asynchrones de chargement de données, sont immédiatement là. Ici, ils attendent leur tour de traiter.

La boucle d'événements donne la priorité à ce qui se trouve dans la pile des appels. Tout d'abord, il fait tout ce qu'il parvient à trouver sur la pile, et une fois la pile vide, il procÚde au traitement de ce qui se trouve dans la file d'attente des événements.

Nous n'avons pas besoin d'attendre qu'une fonction comme setTimeout() finisse de fonctionner, car des fonctions similaires sont fournies par le navigateur et utilisent leurs propres flux. Ainsi, par exemple, en dĂ©finissant le minuteur sur 2 secondes Ă  l'aide de la fonction setTimeout() , vous ne devriez pas, aprĂšs avoir arrĂȘtĂ© l'exĂ©cution d'un autre code, attendre ces 2 secondes, car le minuteur fonctionne en dehors de votre code.

ES6 Job Queue


ECMAScript 2015 (ES6) a introduit le concept de Job Queue, qui est utilisĂ© par les promesses (elles sont Ă©galement apparues dans ES6). GrĂące Ă  la file d'attente des travaux, le rĂ©sultat de l'exĂ©cution de la fonction asynchrone peut ĂȘtre utilisĂ© le plus rapidement possible, sans avoir Ă  attendre que la pile d'appels soit effacĂ©e.

Si une promesse est résolue avant la fin de la fonction en cours, le code correspondant sera exécuté immédiatement aprÚs la fin de la fonction en cours.

J'ai trouvĂ© une analogie intĂ©ressante pour ce dont nous parlons. Cela peut ĂȘtre comparĂ© Ă  des montagnes russes dans un parc d'attractions. Une fois que vous avez parcouru la colline et que vous voulez recommencer, vous prenez un billet et montez en queue de file. Voici comment fonctionne la file d'attente des Ă©vĂ©nements. Mais la file d'attente des travaux est diffĂ©rente. Ce concept est similaire Ă  un billet Ă  prix rĂ©duit, qui vous donne le droit de faire le prochain voyage immĂ©diatement aprĂšs avoir terminĂ© le prĂ©cĂ©dent.

Prenons l'exemple suivant:

 const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) =>   resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo() 

Voici ce qui sera sorti aprÚs son exécution:

 foo baz should be right after baz, before bar bar 

Ce que vous pouvez voir ici montre une sérieuse différence entre les promesses (et la construction async / wait, qui est basée sur elles) et les fonctions asynchrones traditionnelles, dont l'exécution est organisée à l'aide de setTimeout() ou d'autres API de la plate-forme utilisée.

process.nextTick ()


La mĂ©thode process.nextTick() interagit avec la boucle d'Ă©vĂ©nements d'une maniĂšre spĂ©ciale. Une tique est un seul cycle complet d'Ă©vĂ©nements. En passant la fonction Ă  la mĂ©thode process.nextTick() , nous informons le systĂšme que cette fonction doit ĂȘtre appelĂ©e une fois l'itĂ©ration en cours de la boucle d'Ă©vĂ©nements terminĂ©e, avant le dĂ©but de la suivante. L'utilisation de cette mĂ©thode ressemble Ă  ceci:

 process.nextTick(() => { // -  }) 

Supposons qu'une boucle d'événement soit occupée à exécuter du code pour la fonction actuelle. Une fois cette opération terminée, le moteur JavaScript exécutera toutes les fonctions passées à process.nextTick() lors de l'opération précédente. En utilisant ce mécanisme, nous nous efforçons de nous assurer qu'une certaine fonction est exécutée de maniÚre asynchrone (aprÚs la fonction actuelle), mais dÚs que possible, sans la placer dans la file d'attente.

Par exemple, si vous utilisez la construction setTimeout(() => {}, 0) , la fonction sera exĂ©cutĂ©e Ă  la prochaine itĂ©ration de la boucle d'Ă©vĂ©nements, c'est-Ă -dire bien plus tard que lors de l'utilisation de process.nextTick() dans la mĂȘme situation. Cette mĂ©thode doit ĂȘtre utilisĂ©e lorsqu'il est nĂ©cessaire de garantir l'exĂ©cution de code au tout dĂ©but de la prochaine itĂ©ration de la boucle d'Ă©vĂ©nements.

setImmediate ()


Une autre fonction fournie par Node.js pour l'exécution de code asynchrone est setImmediate() . Voici comment l'utiliser:

 setImmediate(() => { //   }) 

La fonction de rappel passée à setImmediate() sera exécutée à la prochaine itération de la boucle d'événement.

En quoi setImmediate() différent de setTimeout(() => {}, 0) (c'est-à-dire d'un minuteur qui devrait fonctionner dÚs que possible) et de process.nextTick() ?

La fonction passée à process.nextTick() s'exécutera une fois l'itération en cours de la boucle d'événements terminée. Autrement dit, une telle fonction sera toujours exécutée avant la fonction dont l'exécution est planifiée à l'aide de setTimeout() ou setImmediate() .

L'appel de la fonction setTimeout() avec un délai défini de 0 ms est trÚs similaire à l'appel de setImmediate() . L'ordre d'exécution des fonctions qui leur sont transférées dépend de divers facteurs, mais dans les deux cas, des rappels seront appelés à la prochaine itération de la boucle d'événements.

Minuteries


Nous avons déjà parlé de la fonction setTimeout() , qui vous permet de planifier des appels aux rappels qui lui sont passés. Prenons un peu de temps pour décrire plus en détail ses fonctionnalités et considérons une autre fonction, setInterval() , similaire à celle-ci. Dans Node.js, les fonctions de travail avec les temporisateurs sont incluses dans le module temporisateur , mais vous pouvez les utiliser sans connecter ce module dans le code, car elles sont globales.

▍ fonction setTimeout ()


Rappelez-vous que lorsque vous appelez la fonction setTimeout() , elle reçoit un rappel et l'heure, en millisecondes, aprÚs laquelle le rappel sera appelé. Prenons un exemple:

 setTimeout(() => { //   2  }, 2000) setTimeout(() => { //   50  }, 50) 

Ici, nous passons setTimeout() nouvelle fonction qui est immédiatement décrite, mais ici nous pouvons utiliser la fonction existante en passant setTimeout() son nom et un ensemble de paramÚtres pour l'exécuter. Cela ressemble à ceci:

 const myFunction = (firstParam, secondParam) => { //   } //   2  setTimeout(myFunction, 2000, firstParam, secondParam) 

La fonction setTimeout() renvoie un identifiant de temporisateur. Habituellement, il n'est pas utilisé, mais vous pouvez l'enregistrer et, si nécessaire, supprimer le minuteur si le rappel planifié n'est plus nécessaire:

 const id = setTimeout(() => { //      2  }, 2000) //  ,       clearTimeout(id) 

▍ ZĂ©ro retard


Dans les sections précédentes, nous avons utilisé setTimeout() , en le passant, comme le temps aprÚs lequel il est nécessaire d'appeler le rappel, 0 . Cela signifiait que le rappel serait appelé dÚs que possible, mais aprÚs l'achÚvement de la fonction actuelle:

 setTimeout(() => { console.log('after ') }, 0) console.log(' before ') 

Un tel code affichera les éléments suivants:

 before after 

Cette technique est particuliĂšrement utile dans les situations oĂč, lors de l'exĂ©cution de tĂąches de calcul lourdes, je ne voudrais pas bloquer le thread principal, permettant Ă  d'autres fonctions d'ĂȘtre exĂ©cutĂ©es, divisant ces tĂąches en plusieurs Ă©tapes, exĂ©cutĂ©es en tant setTimeout() .

Si nous rappelons la fonction setImmediate() ci-dessus, alors elle est standard dans Node.js, ce qui ne peut pas ĂȘtre dit Ă  propos des navigateurs (elle est implĂ©mentĂ©e dans IE et Edge, mais pas dans d'autres).

▍ fonction setInterval ()


La fonction setInterval() est similaire Ă  setTimeout() , mais il existe des diffĂ©rences entre elles. Au lieu d'exĂ©cuter le rappel qui lui est passĂ© une fois, setInterval() appellera pĂ©riodiquement, avec l'intervalle spĂ©cifiĂ©, ce rappel. Cela continuera, idĂ©alement, jusqu'au moment oĂč le programmeur arrĂȘtera explicitement ce processus. Voici comment utiliser cette fonctionnalitĂ©:

 setInterval(() => { //   2  }, 2000) 

Un rappel passĂ© Ă  la fonction ci-dessus sera appelĂ© toutes les 2 secondes. Afin de fournir la possibilitĂ© d'arrĂȘter ce processus, vous devez obtenir l'identifiant du temporisateur retournĂ© par setInterval() et utiliser la commande clearInterval() :

 const id = setInterval(() => { //   2  }, 2000) clearInterval(id) 

Une technique courante consiste à appeler clearInterval() à l'intérieur du rappel passé à setInterval() lorsqu'une certaine condition est remplie. Par exemple, le code suivant sera exécuté périodiquement jusqu'à ce que la propriété App.somethingIWait soit App.somethingIWait sur arrived :

 const interval = setInterval(function() { if (App.somethingIWait === 'arrived') {   clearInterval(interval)   //    -  ,   -    } }, 100) 

▍ RĂ©glage rĂ©cursif setTimeout ()


La fonction setInterval() appellera le rappel qui lui est passé toutes les n millisecondes, sans se soucier de savoir si ce rappel s'est terminé aprÚs son appel précédent.

Si chaque appel Ă  ce rappel nĂ©cessite toujours le mĂȘme temps infĂ©rieur Ă  n , aucun problĂšme ne se pose ici.


AppelĂ© pĂ©riodiquement, chaque session d'exĂ©cution prend le mĂȘme temps, se situant dans l'intervalle entre les appels

Peut-ĂȘtre que cela prend un temps diffĂ©rent pour terminer un rappel, qui est toujours infĂ©rieur Ă  n . Si, par exemple, nous parlons d'effectuer certaines opĂ©rations de rĂ©seau, alors cette situation est tout Ă  fait attendue.


Appelé périodiquement, chaque session d'exécution prend un temps différent, se situant entre les appels

Lorsque vous utilisez setInterval() , une situation peut se produire lorsque le rappel prend plus de n , ce qui conduit à l'appel suivant avant la fin de l'appel précédent.


Appelé périodiquement, chaque session prend un temps différent, qui parfois ne correspond pas à l'intervalle entre les appels

Afin d'éviter cette situation, vous pouvez utiliser la technique de réglage de minuterie récursive en utilisant setTimeout() . Le fait est que le prochain rappel est prévu aprÚs la fin de son précédent appel:

 const myFunction = () => { //    setTimeout(myFunction, 1000) } setTimeout( myFunction() }, 1000) 

Avec cette approche, le scĂ©nario suivant peut ĂȘtre implĂ©mentĂ©:


Un appel récursif à setTimeout () pour planifier l'exécution du rappel

Résumé


Aujourd'hui, nous avons parlé des mécanismes internes de Node.js, tels que la boucle d'événements, la pile d'appels, et discuté du travail avec des temporisateurs qui vous permettent de planifier l'exécution de code. La prochaine fois, nous aborderons le sujet de la programmation asynchrone.

Chers lecteurs! Avez-vous rencontrĂ© des situations oĂč vous avez dĂ» utiliser process.nextTick ()?

Source: https://habr.com/ru/post/fr424553/


All Articles