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.

[Nous vous conseillons de lire] Autres parties du cyclePartie 1:
Informations générales et mise en routePartie 2:
JavaScript, V8, quelques astuces de développementPartie 3:
Hébergement, REPL, travailler avec la console, les modulesPartie 4:
fichiers npm, package.json et package-lock.jsonPartie 5:
npm et npxPartie 6:
boucle d'événements, pile d'appels, temporisateursPartie 7:
Programmation asynchronePartie 8:
Guide Node.js, Partie 8: Protocoles HTTP et WebSocketPartie 9:
Guide Node.js, partie 9: utilisation du système de fichiersPartie 10:
Guide Node.js, Partie 10: Modules standard, flux, bases de données, NODE_ENVPDF complet du guide Node.js 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(() => {
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)
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)
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?