Manuel Node.js, partie 7: programmation asynchrone

Aujourd'hui, dans la traduction de la septième partie du manuel Node.js, nous parlerons de la programmation asynchrone, examinerons des questions telles que l'utilisation des rappels, des promesses et de la construction async / wait, et discuterons de la façon de travailler avec les événements.




Asynchronie dans les langages de programmation


JavaScript lui-même est un langage de programmation synchrone à un seul thread. Cela signifie que vous ne pouvez pas créer de nouveaux threads dans le code qui s'exécutent en parallèle. Cependant, les ordinateurs sont intrinsèquement asynchrones. Autrement dit, certaines actions peuvent être effectuées quel que soit le flux d'exécution du programme principal. Dans les ordinateurs modernes, chaque programme est alloué une certaine quantité de temps processeur, lorsque ce temps est écoulé, le système donne des ressources à un autre programme, également pendant un certain temps. Ces commutations sont effectuées de manière cyclique, cela se fait si rapidement qu'une personne ne peut tout simplement pas le remarquer, en conséquence, nous pensons que nos ordinateurs exécutent de nombreux programmes simultanément. Mais c'est une illusion (sans parler des machines multiprocesseurs).

Dans les entrailles des programmes, des interruptions sont utilisées - des signaux transmis au processeur et permettant d'attirer l'attention du système. Nous n'entrerons pas dans les détails, le plus important est de se rappeler que le comportement asynchrone, lorsque le programme est suspendu jusqu'au moment où il a besoin de ressources processeur, est tout à fait normal. À un moment où le programme ne charge pas le système de travail, l'ordinateur peut résoudre d'autres problèmes. Par exemple, avec cette approche, lorsqu'un programme attend une réponse à une requête réseau qui lui est adressée, il ne bloque pas le processeur tant qu'une réponse n'est pas reçue.

En règle générale, les langages de programmation sont asynchrones, certains d'entre eux donnent au programmeur la possibilité de contrôler les mécanismes asynchrones, à l'aide des outils de langage intégrés ou de bibliothèques spécialisées. Nous parlons de langages tels que C, Java, C #, PHP, Go, Ruby, Swift, Python. Certains d'entre eux vous permettent de programmer en style asynchrone, en utilisant des threads, en démarrant de nouveaux processus.

Asynchronie JavaScript


Comme déjà mentionné, JavaScript est un langage synchrone à un seul thread. Les lignes de code écrites en JS sont exécutées dans l'ordre dans lequel elles apparaissent dans le texte, l'une après l'autre. Par exemple, voici un programme JS très normal qui illustre ce comportement:

const a = 1 const b = 2 const c = a * b console.log(c) doSomething() 

Mais JavaScript a été créé pour être utilisé dans les navigateurs. Sa tâche principale, au tout début, était d'organiser le traitement des événements liés aux activités des utilisateurs. Par exemple, ce sont des événements tels que onClick , onSubmit , onSubmit , onSubmit , etc. Comment résoudre de tels problèmes dans le cadre d'un modèle de programmation synchrone?

La réponse réside dans l'environnement dans lequel JavaScript s'exécute. À savoir, le navigateur vous permet de résoudre efficacement ces problèmes, en donnant au programmeur les API appropriées.

Dans l'environnement de Node.js, il existe des outils pour effectuer des opérations d'E / S non bloquantes, comme travailler avec des fichiers, organiser l'échange de données sur un réseau, etc.

Rappels


Si nous parlons de JavaScript basé sur un navigateur, on peut noter qu'il est impossible de savoir à l'avance lorsque l'utilisateur clique sur un bouton. Afin de garantir que le système réponde à un tel événement, un gestionnaire est créé pour lui.

Le gestionnaire d'événements accepte une fonction qui sera appelée lorsque l'événement se produit. Cela ressemble à ceci:

 document.getElementById('button').addEventListener('click', () => { //    }) 

Ces fonctions sont également appelées fonctions de rappel ou rappels.

Un rappel est une fonction régulière qui est passée en tant que valeur à une autre fonction. Il sera appelé uniquement lorsqu'un certain événement se produit. JavaScript implémente le concept de fonctions de première classe. Ces fonctions peuvent être affectées à des variables et transmises à d'autres fonctions (appelées fonctions d'ordre supérieur).

Une approche courante dans le développement JavaScript côté client est lorsque tout le code client est enveloppé dans un écouteur de l'événement de load d'un objet window , qui appelle le rappel qui lui est passé une fois que la page est prête à fonctionner:

 window.addEventListener('load', () => { //  //     }) 

Les rappels sont utilisés partout, et pas seulement pour gérer les événements DOM. Par exemple, nous avons déjà rencontré leur utilisation dans les minuteries:

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

Les demandes XHR utilisent également des rappels. Dans ce cas, cela ressemble à l'attribution d'une fonction à la propriété correspondante. Une fonction similaire sera appelée lorsqu'un certain événement se produit. Dans l'exemple suivant, un tel événement est un changement d'état de la demande:

 const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) {   xhr.status === 200 ? console.log(xhr.responseText) : console.error('error') } } xhr.open('GET', 'https://yoursite.com') xhr.send() 

▍ Gestion des erreurs dans les rappels


Parlons de la façon de gérer les erreurs dans les rappels. Il existe une stratégie commune pour gérer ces erreurs, qui est également utilisée dans Node.js. Il consiste dans le fait que le premier paramètre de toute fonction de rappel est un objet d'erreur. S'il n'y a aucune erreur, null sera écrit dans ce paramètre. Sinon, il y aura un objet d'erreur contenant sa description et des informations supplémentaires à son sujet. Voici à quoi ça ressemble:

 fs.readFile('/file.json', (err, data) => { if (err !== null) {   //    console.log(err)   return } // ,   console.log(data) }) 

▍ Problème de rappel


Les rappels sont pratiques à utiliser dans des situations simples. Cependant, chaque rappel est un niveau supplémentaire d'imbrication de code. Si plusieurs rappels imbriqués sont utilisés, cela conduit rapidement à une complication importante de la structure du code:

 window.addEventListener('load', () => { document.getElementById('button').addEventListener('click', () => {   setTimeout(() => {     items.forEach(item => {       //,  -      })   }, 2000) }) }) 

Dans cet exemple, seuls 4 niveaux de code sont affichés, mais en pratique, on peut rencontrer un grand nombre de niveaux, généralement appelés «enfer de rappel». Vous pouvez résoudre ce problème en utilisant d'autres constructions de langage.

Promesses et asynchronisation / attente


À partir de la norme ES6, JavaScript introduit de nouvelles fonctionnalités qui facilitent l'écriture de code asynchrone, éliminant ainsi le besoin de rappels. Nous parlons des promesses qui sont apparues dans ES6 et de la construction async / wait qui est apparue dans ES8.

▍ Promesses


Les promesses (objets de promesse) sont l'une des façons de travailler avec des constructions logicielles asynchrones en JavaScript, ce qui, en général, réduit l'utilisation des rappels.

Connaissance des promesses


Les promesses sont généralement définies comme des objets proxy pour certaines valeurs, dont l'apparition est attendue à l'avenir. Les promesses sont également appelées «promesses» ou «résultats promis». Bien que ce concept existe depuis de nombreuses années, les promesses ont été normalisées et ajoutées au langage uniquement dans ES2015. Dans ES2017, la conception asynchrone / attendent, qui est basée sur des promesses et qui peut être considérée comme leur remplacement pratique, est apparue. Par conséquent, même si vous ne prévoyez pas d'utiliser des promesses régulières, il est important de comprendre comment elles fonctionnent pour une utilisation efficace de la construction async / wait.

Comment fonctionnent les promesses


Une fois qu'une promesse est appelée, elle passe dans un état en attente. Cela signifie que la fonction qui a provoqué la promesse continue d'être exécutée, tandis que certains calculs sont effectués dans la promesse, après quoi la promesse en informe. Si l'opération effectuée par la promesse se termine avec succès, la promesse est alors transférée à l'état rempli. On dit qu'une telle promesse a été menée à bien. Si l'opération se termine avec une erreur, la promesse est placée à l'état rejeté.

Parlons de travailler avec des promesses.

Créer des promesses


L'API pour travailler avec des promesses nous donne le constructeur correspondant, qui est appelé par une commande de la forme new Promise() . Voici comment les promesses sont créées:

 let done = true const isItDoneYet = new Promise( (resolve, reject) => {   if (done) {     const workDone = 'Here is the thing I built'     resolve(workDone)   } else {     const why = 'Still working on something else'     reject(why)   } } ) 

Promis vérifie la constante globale done et si sa valeur est true , elle est résolue avec succès. Sinon, la promesse est rejetée. En utilisant les paramètres de resolve et de reject , qui sont des fonctions, nous pouvons renvoyer des valeurs de la promesse. Dans ce cas, nous renvoyons une chaîne, mais ici un objet peut être utilisé.

Travailler avec des promesses


Nous avons créé une promesse ci-dessus, envisagez maintenant de travailler avec elle. Cela ressemble à ceci:

 const isItDoneYet = new Promise( //... ) const checkIfItsDone = () => { isItDoneYet   .then((ok) => {     console.log(ok)   })   .catch((err) => {     console.error(err)   }) } checkIfItsDone() 

L'appel de checkIfItsDone() entraînera l'exécution de la isItDoneYet() isItDoneYet isItDoneYet() et l'organisation de l'attente de sa résolution. Si la promesse se résout avec succès, le rappel passé à la méthode .then() fonctionnera. Si une erreur se produit, c'est-à-dire que la promesse sera rejetée, elle peut être traitée dans la fonction passée à la méthode .catch() .

Enchaîner les promesses


Les méthodes de promesse renvoient des promesses, ce qui vous permet de les combiner en chaînes. Un bon exemple de ce comportement est l' API Fetch basée sur navigateur, qui est une couche d'abstraction sur XMLHttpRequest . Il existe un package npm assez populaire pour Node.js qui implémente l'API Fetch, dont nous discuterons plus tard. Cette API peut être utilisée pour charger certaines ressources du réseau et, grâce à la possibilité de combiner des promesses en chaîne, pour organiser le traitement ultérieur des données téléchargées. En fait, lorsque vous appelez l'API Fetch via un appel à la fonction fetch() , une promesse est créée.

Prenons l'exemple suivant de l'enchaînement des promesses:

 const fetch = require('node-fetch') const status = (response) => { if (response.status >= 200 && response.status < 300) {   return Promise.resolve(response) } return Promise.reject(new Error(response.statusText)) } const json = (response) => response.json() fetch('https://jsonplaceholder.typicode.com/todos') .then(status) .then(json) .then((data) => { console.log('Request succeeded with JSON response', data) }) .catch((error) => { console.log('Request failed', error) }) 

Ici, nous utilisons le package npm -fetch et la ressource jsonplaceholder.typicode.com comme source de données JSON.

Dans cet exemple, la fonction fetch() est utilisée pour charger un élément de liste TODO à l'aide d'une chaîne de promesses. Après avoir exécuté fetch() , une réponse est retournée qui a de nombreuses propriétés, parmi lesquelles nous nous intéressons aux suivantes:

  • status est une valeur numérique qui représente le code d'état HTTP.
  • statusText - une description textuelle du code d'état HTTP, qui est représenté par la chaîne OK si la demande a réussi.

L'objet de response a une méthode json() qui renvoie une promesse, à la résolution de laquelle le contenu traité du corps de la requête est présenté, présenté au format JSON.

Compte tenu de ce qui précède, nous décrivons ce qui se passe dans ce code. La première promesse de la chaîne est représentée par la fonction status() que nous avons annoncée, qui vérifie le statut de la réponse, et si elle indique que la demande a échoué (c'est-à-dire que le code de statut HTTP n'est pas compris entre 200 et 299), la promesse est rejetée. Cette opération conduit au fait que les autres expressions .then() dans la chaîne de promesses ne sont pas exécutées et nous arrivons immédiatement à la méthode .catch() , sortie sur la console, avec le message d'erreur, le texte Request failed .

Si le code d'état HTTP nous convient, la fonction json() déclarée par nous est appelée. Étant donné que la promesse précédente, si elle est résolue avec succès, renvoie un objet de response , nous l'utilisons comme valeur d'entrée pour la deuxième promesse.

Dans ce cas, nous renvoyons les données JSON traitées, de sorte que la troisième promesse les reçoive, après quoi, précédées d'un message indiquant que la demande a permis d'obtenir les données nécessaires, sont affichées dans la console.

Gestion des erreurs


Dans l'exemple précédent, nous avions une méthode .catch() attachée à une chaîne de promesses. Si quelque chose se passe mal dans la chaîne de promesses et qu'une erreur se produit, ou si l'une des promesses se révèle rejetée, le contrôle est transféré vers l'expression la plus proche .catch() . Voici la situation lorsqu'une erreur se produit dans une promesse:

 new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) 

Voici un exemple de déclenchement de .catch() après avoir rejeté une promesse:

 new Promise((resolve, reject) => { reject('Error') }) .catch((err) => { console.error(err) }) 

Gestion des erreurs en cascade


Que faire si une erreur se produit dans l'expression .catch() ? Pour gérer cette erreur, vous pouvez inclure une autre expression .catch() dans la chaîne de promesses (puis vous pouvez attacher autant d'expressions .catch() à la chaîne que nécessaire):

 new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) 

Voyons maintenant quelques méthodes utiles pour gérer les promesses.

Promise.all ()


Si vous devez effectuer une action après avoir résolu plusieurs promesses, vous pouvez le faire à l'aide de la commande Promise.all() . Prenons un exemple:

 const f1 = fetch('https://jsonplaceholder.typicode.com/todos/1') const f2 = fetch('https://jsonplaceholder.typicode.com/todos/2') Promise.all([f1, f2]).then((res) => {   console.log('Array of results', res) }) .catch((err) => { console.error(err) }) 

Dans ES2015, la syntaxe de l'affectation destructive est apparue; en l'utilisant, vous pouvez créer des constructions de la forme suivante:

 Promise.all([f1, f2]).then(([res1, res2]) => {   console.log('Results', res1, res2) }) 

Ici, à titre d'exemple, nous avons considéré l'API Fetch, mais Promise.all() , bien sûr, vous permet de travailler avec toutes les promesses.

Promise.race ()


La commande Promise.race() vous permet d'effectuer l'action spécifiée une fois qu'une des promesses qui lui a été transmise est résolue. Le rappel correspondant contenant les résultats de cette première promesse n'est appelé qu'une seule fois. Prenons un exemple:

 const first = new Promise((resolve, reject) => {   setTimeout(resolve, 500, 'first') }) const second = new Promise((resolve, reject) => {   setTimeout(resolve, 100, 'second') }) Promise.race([first, second]).then((result) => { console.log(result) // second }) 

Erreur TypeError non interceptée qui se produit lors de l'utilisation de promesses


Si, lorsque vous travaillez avec des promesses, vous rencontrez l' Uncaught TypeError: undefined is not a promise erreur de Uncaught TypeError: undefined is not a promise , assurez-vous que la new Promise() construction new Promise() est utilisée au lieu de simplement Promise() lors de la création de promesses.

▍ conception asynchrone / en attente


La construction async / wait est une approche moderne de la programmation asynchrone, la simplifiant. Les fonctions asynchrones peuvent être représentées comme une combinaison de promesses et de générateurs, et, en général, cette construction est une abstraction sur les promesses.

La conception asynchrone / attente réduit la quantité de code passe-partout que vous devez écrire lorsque vous travaillez avec des promesses. Lorsque des promesses sont apparues dans la norme ES2015, elles visaient à résoudre le problème de la création de code asynchrone. Ils ont fait face à cette tâche, mais en deux ans, partageant la sortie des normes ES2015 et ES2017, il est devenu clair qu'ils ne pouvaient pas être considérés comme la solution finale au problème.

L'un des problèmes que les promesses ont résolu était le fameux «enfer des rappels», mais ils, résolvant ce problème, ont créé leurs propres problèmes de nature similaire.

Les promesses étaient de simples constructions autour desquelles on pouvait construire quelque chose avec une syntaxe plus simple. Par conséquent, le moment venu, la construction async / wait est apparue. Son utilisation vous permet d'écrire du code qui ressemble à synchrone, mais il est asynchrone, en particulier, il ne bloque pas le thread principal.

Fonctionnement de la construction asynchrone / attendent


Une fonction asynchrone renvoie une promesse, comme dans l'exemple suivant:

 const doSomethingAsync = () => {   return new Promise((resolve) => {       setTimeout(() => resolve('I did something'), 3000)   }) } 

Lorsque vous devez appeler une fonction similaire, vous devez placer le mot-clé await avant la commande pour l'appeler. Cela entraînera le code appelant à attendre l'autorisation ou le rejet de la promesse correspondante. Il convient de noter qu'une fonction qui utilise le mot-clé await doit être déclarée à l'aide du async - async :

 const doSomething = async () => {   console.log(await doSomethingAsync()) } 

Combinez les deux fragments de code ci-dessus et examinez son comportement:

 const doSomethingAsync = () => {   return new Promise((resolve) => {       setTimeout(() => resolve('I did something'), 3000)   }) } const doSomething = async () => {   console.log(await doSomethingAsync()) } console.log('Before') doSomething() console.log('After') 

Ce code affichera les éléments suivants:

 Before After I did something 

Le texte que I did something entre dans la console avec un retard de 3 secondes.

À propos des promesses et des fonctions asynchrones


Si vous déclarez une certaine fonction à l'aide du async - async , cela signifie qu'une telle fonction renverra une promesse même si elle n'est pas explicitement effectuée. C'est pourquoi, par exemple, l'exemple suivant est un code de travail:

 const aFunction = async () => { return 'test' } aFunction().then(console.log) //    'test' 

Cette conception est similaire à ceci:

 const aFunction = async () => { return Promise.resolve('test') } aFunction().then(console.log) //    'test' 

Points forts de l'async / attente


En analysant les exemples ci-dessus, vous pouvez voir que le code qui utilise async / wait est plus simple que le code qui utilise le chaînage de promesses, ou un code basé sur des fonctions de rappel. Ici, bien sûr, nous avons examiné des exemples très simples. Vous pouvez profiter pleinement des avantages ci-dessus en travaillant avec du code beaucoup plus complexe. Voici, par exemple, comment charger et analyser des données JSON à l'aide de promesses:

 const getFirstUserData = () => { return fetch('/users.json') //      .then(response => response.json()) //  JSON   .then(users => users[0]) //      .then(user => fetch(`/users/${user.name}`)) //       .then(userResponse => userResponse.json()) //  JSON } getFirstUserData() 

Voici à quoi ressemble la solution au même problème en utilisant async / wait:

 const getFirstUserData = async () => { const response = await fetch('/users.json') //    const users = await response.json() //  JSON const user = users[0] //    const userResponse = await fetch(`/users/${user.name}`) //     const userData = await userResponse.json() //  JSON return userData } getFirstUserData() 

Utilisation de séquences de fonctions asynchrones


Les fonctions asynchrones peuvent facilement être combinées dans des conceptions qui ressemblent à des chaînes Promise. Les résultats d'une telle combinaison sont cependant d'une bien meilleure lisibilité:

 const promiseToDoSomething = () => {   return new Promise(resolve => {       setTimeout(() => resolve('I did something'), 10000)   }) } const watchOverSomeoneDoingSomething = async () => {   const something = await promiseToDoSomething()   return something + ' and I watched' } const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {   const something = await watchOverSomeoneDoingSomething()   return something + ' and I watched as well' } watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {   console.log(res) }) 

Ce code affichera le texte suivant:

 I did something and I watched and I watched as well 

Débogage simplifié


Les promesses sont difficiles à déboguer, car en les utilisant, vous ne pouvez pas utiliser efficacement les outils habituels du débogueur (comme "bypass d'étape", step-over). Le code écrit en utilisant async / expect peut être débogué en utilisant les mêmes méthodes que le code synchrone normal.

Génération d'événements dans Node.js


Si vous avez travaillé avec JavaScript dans un navigateur, vous savez que les événements jouent un rôle énorme dans la gestion des interactions des utilisateurs avec les pages. Il s'agit de gérer les événements provoqués par les clics et les mouvements de la souris, les frappes sur le clavier, etc. Dans Node.js, vous pouvez travailler avec des événements que le programmeur crée lui-même. Ici, vous pouvez créer votre propre système d'événements à l'aide du module d' événements . En particulier, ce module nous offre la classe EventEmitter , dont les capacités peuvent être utilisées pour organiser le travail avec les événements. Avant d'utiliser ce mécanisme, vous devez le connecter:

 const EventEmitter = require('events').EventEmitter 

Lorsque vous travaillez avec, les méthodes on on() et emit() sont disponibles, entre autres. La méthode emit utilisée pour appeler des événements. La méthode on est utilisée pour configurer les rappels, les gestionnaires d'événements qui sont appelés lorsqu'un certain événement est appelé.

Par exemple, créons un événement de start . Lorsque cela se produit, nous afficherons quelque chose sur la console:

 eventEmitter = new EventEmitter(); eventEmitter.on('start', () => { console.log('started') }) 

Pour déclencher cet événement, la construction suivante est utilisée:

 eventEmitter.emit('start') 

À la suite de cette commande, le gestionnaire d'événements est appelé et la chaîne started arrive à la console.

Vous pouvez passer des arguments au gestionnaire d'événements, en les représentant comme des arguments supplémentaires à la méthode emit() :

 eventEmitter.on('start', (number) => { console.log(`started ${number}`) }) eventEmitter.emit('start', 23) 

La même chose se produit dans les cas où le gestionnaire doit passer plusieurs arguments:

 eventEmitter.on('start', (start, end) => { console.log(`started from ${start} to ${end}`) }) eventEmitter.emit('start', 1, 100) 

EventEmitter classe EventEmitter ont d'autres méthodes utiles:

  • once() - vous permet d'enregistrer un gestionnaire d'événements qui ne peut être appelé qu'une seule fois.
  • removeListener() - vous permet de supprimer le gestionnaire qui lui est transmis du tableau des gestionnaires de l'événement qui lui est transmis.
  • removeAllListeners() - vous permet de supprimer tous les gestionnaires de l'événement qui lui sont passés.

Résumé


Aujourd'hui, nous avons parlé de la programmation asynchrone en JavaScript, en particulier, nous avons discuté des rappels, des promesses et de la construction async / wait. Ici, nous avons abordé la question de l'utilisation des événements décrits par le développeur à l'aide du module d' events . Notre prochain sujet sera les mécanismes de mise en réseau de la plate-forme Node.js.

Chers lecteurs! Lors de la programmation de Node.js, utilisez-vous la construction async / wait?

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


All Articles