Écriture de tâches asynchrones dans JavaScript moderne
JavaScript présente deux caractéristiques principales en tant que langage de programmation, qui sont toutes deux importantes pour comprendre le fonctionnement de notre code. La première est sa nature synchrone ce qui signifie que le code va courir ligne après ligne, presque comme vous l'avez lu, et deuxièmement qu'il est à un seul thread uniquement une commande est en cours d'exécution à tout moment.
À mesure que le langage évoluait, de nouveaux artefacts apparaissaient dans la scène pour permettre l'exécution asynchrone. Les développeurs ont essayé différentes approches tout en résolvant des algorithmes et des flux de données plus complexes, ce qui a conduit à l'émergence de nouvelles interfaces et de nouveaux modèles les entourant.
Synchronous Execution And The Observer Pattern
Comme indiqué dans l'introduction, JavaScript exécute le code que vous écrivez. ligne par ligne, la plupart du temps. Même dans ses premières années, le langage comportait des exceptions à cette règle, même si elles étaient rares et que vous les connaissez peut-être déjà: requêtes HTTP, événements DOM et intervalles de temps.
Si nous ajoutons un écouteur d'événements pour répondre au clic de un élément de l'utilisateur, peu importe le fonctionnement de l'interpréteur de langage, il s'arrête lance le code que nous avons écrit dans le rappel de l'auditeur puis revient à son flux normal.
Idem avec un intervalle ou une requête réseau, addEventListener
setTimeout
et XMLHttpRequest
ont été les premiers artefacts permettant d'accéder à une exécution asynchrone pour les développeurs Web.
Bien que ces derniers soient À l’exception de l’exécution synchrone en JavaScript, il est essentiel de comprendre que le langage est toujours à thread unique. Nous pouvons rompre cette synchronicité mais l’interprète exécutera toujours une ligne de code à la fois.
Par exemple, vérifions une requête réseau.
var request = new XMLHttpRequest ();
request.open ('GET', '//some.api.at/server', true);
// observe la réponse du serveur
request.onreadystatechange = function () {
if (request.readyState === 4 && xhr.status === 200) {
console.log (request.responseText);
}
}
demande envoyée();
Peu importe ce qui se passe, au moment du retour du serveur, la méthode affectée à onreadystatechange
est appelée avant de reprendre la séquence de code du programme.
Une réaction similaire se produit lors de la réaction à une interaction utilisateur.
const button = document.querySelector ('bouton');
// observe l'interaction de l'utilisateur
button.addEventListener ('click', fonction (e) {
console.log ('l'utilisateur clique juste ce qui s'est passé!');
})
Vous remarquerez peut-être que nous nous connectons à un événement externe et passons un rappel, indiquant au code ce qu'il doit faire lorsqu'il se produit. Il y a plus de dix ans, “Qu'est-ce qu'un rappel?” Était une question plutôt attendue lors d'une interview, car cette tendance était présente dans la plupart des bases de code.
Dans chaque cas mentionné, nous réagissons à un événement externe. Un certain intervalle de temps atteint, une action de l'utilisateur ou une réponse du serveur. Nous n'étions pas en mesure de créer une tâche asynchrone en soi, nous avons toujours observé des événements se déroulant hors de notre portée.
C'est pourquoi le code ainsi façonné s'appelle le Observer Pattern . , qui est mieux représenté par l'interface addEventListener
dans ce cas.
Node.js et les émetteurs d’événements
Un bon exemple est Node.js, dont la page se décrit comme «un environnement d’exécution JavaScript asynchrone piloté par événement», donc émetteurs d’événements et rappel. étaient des citoyens de première classe. Il y avait même un constructeur EventEmitter
déjà implémenté.
const EventEmitter = require ('events');
const emitter = new EventEmitter ();
// répondre aux événements
emitter.on ('gretting', (message) => console.log (message));
// envoyer des événements
emitter.emit ('gretting', 'Salut à tous!');
Il s'agissait non seulement de l'approche à suivre pour l'exécution asynchrone, mais également d'un modèle et de la convention de base de son écosystème. Node.js a ouvert une nouvelle ère dans l'écriture de JavaScript dans un environnement différent, même en dehors du Web. En conséquence, d'autres situations asynchrones étaient possibles, telles que la création de nouveaux répertoires ou l'écriture de fichiers.
const {mkdir, writeFile} = require ('fs');
const styles = 'body {background: #ffdead; } ';
mkdir ('./ assets /', (error) => {
si (! erreur) {
writeFile ('assets / main.css', styles, 'utf-8', (error) => {
if (! erreur) console.log ('feuille de style créée');
})
}
})
Vous remarquerez peut-être que les rappels reçoivent une erreur
comme premier argument; si des données de réponse sont attendues, elles constituent un second argument. Cela s'appelait Error-first Callback Pattern qui est devenu une convention que les auteurs et les contributeurs ont adoptée pour leurs propres packages et bibliothèques.
Promises et la chaîne de rappel illimitée
Le développement Web faisant face à des problèmes plus complexes à résoudre, le besoin de meilleurs artefacts asynchrones est apparu. Si nous regardons le dernier extrait de code, nous pouvons voir un enchaînement de rappels répété, qui ne s'adapte pas correctement à mesure que le nombre de tâches augmente (par exemple).
Par exemple, ajoutons seulement deux étapes supplémentaires: lecture de fichier et prétraitement des styles. const {mkdir, writeFile, readFile} = require ('fs');
const less = require ('less')
readFile ('./ main.less', 'utf-8', (error, data) => {
si (erreur) jeter erreur
less.render (data, (lessError, sortie) => {
if (lessError) renvoie moinsError
mkdir ('./ assets /', (dirError) => {
if (dirError) renvoie dirError
writeFile ('assets / main.css', output.css, 'utf-8', (writeError) => {
if (writeError) renvoie writeError
console.log ('feuille de style créée');
})
})
})
})
Nous pouvons voir comment, à mesure que le programme que nous écrivons devient plus complexe, le code devient plus difficile à suivre pour l'œil humain en raison de l'enchaînement de plusieurs rappels et de la gestion des erreurs répétées.
Promises, Wrappers And Chain Patterns
Les promesses
n’ont pas retenu beaucoup d’attention lorsqu’elles ont été annoncées comme la nouvelle addition au langage JavaScript, elles ne sont pas un nouveau concept car les autres langages avaient des implémentations similaires des décennies plus tôt. En réalité, ils ont beaucoup changé la sémantique et la structure de la plupart des projets sur lesquels j'ai travaillé depuis son apparition.
Promises
n'a pas seulement introduit une solution intégrée permettant aux développeurs d'écrire du code asynchrone. mais a également ouvert une nouvelle étape dans le développement Web, qui a servi de base de construction à de nouvelles fonctionnalités ultérieures de la spécification Web, telles que fetch
.
La migration d'une méthode vers une approche basée sur les promesses devenait de plus en plus courante. plus habituel dans les projets (tels que les bibliothèques et les navigateurs), et même Node.js a commencé à migrer lentement vers eux.
Enveloppons, par exemple, la méthode de Node readFile
:
const {readFile} = require ('fs');
const asyncReadFile = (chemin, options) => {
renvoyer une nouvelle promesse ((résoudre, rejeter) => {
readFile (chemin, options, (error, data) => {
si (erreur) rejeter (erreur);
sinon résoudre (données);
})
});
}
Nous masquons ici le rappel en exécutant à l'intérieur d'un constructeur Promise, appelant resolvez
lorsque le résultat de la méthode aboutit et rejetez
lorsque l'objet d'erreur est défini.
La méthode retourne un objet Promise
. Nous pouvons suivre sa résolution réussie en passant une fonction à puis
son argument est la valeur à laquelle la promesse a été résolue, dans ce cas, data
.
Si une erreur était commise au cours de la méthode, la fonction capture
sera appelée, si elle est présente.
Note : Si vous avez besoin de mieux comprendre. Pour en savoir plus sur le fonctionnement de Promises, je recommande l'article «[ de JavaScript Promises: Une introduction ] de Jake Archibald, qu'il a écrit sur le blog de développement Web de Google.
Nous pouvons maintenant utiliser ces nouvelles méthodes et éviter les rappels. chaines.
asyncRead ('./ main.less', 'utf-8')
.then (data => console.log ('contenu du fichier', data))
.catch (error => console.error ('quelque chose s'est mal passé', erreur))
Le fait de disposer d'un moyen natif de créer des tâches asynchrones et d'une interface claire pour suivre ses résultats possibles a permis à l'industrie de sortir du modèle Observer. Les codes basés sur des promesses semblaient résoudre le code illisible et sujet aux erreurs.
Plus la mise en surbrillance de la syntaxe ou les messages d'erreur plus clairs aident au codage, plus le code est facile à raisonner pour les développeurs.
L’adoption des promesses
était si globale dans la communauté que Node.js publia rapidement des versions intégrées de ses fonctions. Méthodes d'E / S pour renvoyer des objets Promise tels que leur importation des opérations de fichier à partir de fs.promises
.
Il fournissait même un promisify
util pour envelopper toute fonction qui suivait le premier rappel d'erreur. Modélisez-la et transformez-la en une promesse.
Mais les promesses aident-elles dans tous les cas?
Ré-imaginons notre tâche de prétraitement de style écrite avec Promises.
const {mkdir, writeFile, readFile} = require ('fs'). promises;
const less = require ('less')
readFile ('./ main.less', 'utf-8')
.then (less.render)
.then (result =>
mkdir ('./ assets')
.then (writeFile ('assets / main.css', result.css, 'utf-8'))
)
.catch (error => console.error (error))
Il y a une nette réduction de la redondance dans le code, en particulier en ce qui concerne le traitement des erreurs car nous nous en remettons maintenant à catch
mais Promises n'a pas réussi à fournir une indentation claire du code directement liée à la concaténation d'actions. .
Ceci est effectivement réalisé sur la première déclaration puis
après que soit lue, Fichier
. Après ces lignes, il s’agit de la nécessité de créer une nouvelle portée dans laquelle on peut d’abord créer le répertoire, pour ensuite écrire le résultat dans un fichier. Cela provoque une rupture du rythme d'indentation, ce qui ne facilite pas la détermination de la séquence d'instructions au premier abord.
Un moyen de résoudre ce problème consiste à précuire une méthode personnalisée qui gère cela et permet la concaténation correcte de la méthode, mais nous introduirions une profondeur supplémentaire de complexité dans un code qui semble déjà avoir ce dont il a besoin pour accomplir la tâche que nous voulons.
Note : comptez que ceci est un exemple de programme, et nous maîtrisons certaines des méthodes et elles suivent toutes une convention de l’industrie, mais ce n’est pas toujours le cas. Avec des concaténations plus complexes ou l'introduction d'une bibliothèque de forme différente, notre style de code peut facilement se casser.
Heureusement, la communauté JavaScript a appris à nouveau à partir d'autres syntaxes de langage et a ajouté une notation qui aide beaucoup autour de ces cas. où la concaténation de tâches asynchrones n'est pas aussi agréable ni aussi simple à lire que le code synchrone.
Async And Await
Une promesse est définie comme une valeur non résolue au moment de l'exécution et crée une instance. of
Promise
est un appel explicite de cet artefact.
const {mkdir, writeFile, readFile} = require ('fs'). promesses;
const less = require ('less')
readFile ('./ main.less', 'utf-8')
.then (less.render)
.then (result =>
mkdir ('./ assets')
.then (writeFile ('assets / main.css', result.css, 'utf-8'))
)
.catch (error => console.error (error))
Dans une méthode asynchrone, nous pouvons utiliser le mot réservé wait
pour déterminer la résolution d'une Promise
avant de poursuivre son exécution.
Revisitons ou copions de code en utilisant cette syntaxe. .
const {mkdir, writeFile, readFile} = require ('fs'). Promises;
const less = require ('less')
fonction async. processLess () {
const content = wait readFile ('./ main.less', 'utf-8')
const result = wait less.render (contenu)
wait mkdir ('./ assets')
wait writeFile ('assets / main.css', result.css, 'utf-8')
}
processLess ()
Note : Notez que nous avons dû transférer tout notre code vers une méthode car nous ne pouvons pas utiliser attendre
en dehors du cadre d'une fonction asynchrone aujourd'hui.
Chaque fois qu'une méthode asynchrone trouve une instruction wait
elle cesse de s'exécuter jusqu'à ce que la valeur précédente ou la promesse soit résolue.
L'utilisation de la notation async / wait, en dépit de sa valeur asynchrone, a des conséquences évidentes. lors de l'exécution, le code a l'air d'être synchrone ce que les développeurs ont l'habitude de voir et de raisonner.
Qu'en est-il de la gestion des erreurs? Pour cela, nous utilisons des déclarations présentes de longue date dans le langage, try
et catch
.
const {mkdir, writeFile, readFile} = require ('fs ').promesses;
const less = require ('less')
fonction async. processLess () {
const content = wait readFile ('./ main.less', 'utf-8')
const result = wait less.render (contenu)
wait mkdir ('./ assets')
wait writeFile ('assets / main.css', result.css, 'utf-8')
}
essayer {
processLess ()
} catch (e) {
console.error (e)
}
Nous sommes assurés que toute erreur commise au cours du processus sera traitée par le code contenu dans la déclaration de capture
. Nous avons un endroit centré qui s'occupe de la gestion des erreurs, mais nous avons maintenant un code qui est plus facile à lire et à suivre.
Il n'est pas nécessaire de stocker les actions conséquentes qui retournent une valeur dans des variables comme mkdir
qui ne rompt pas le rythme du code; il n'est pas non plus nécessaire de créer une nouvelle portée pour accéder ultérieurement à la valeur du résultat
.
Il est prudent de dire que les promesses étaient un artefact fondamental introduit dans le langage, nécessaire pour permettre la notation asynchrone / wait en JavaScript, que vous pouvez utiliser à la fois sur les navigateurs modernes et sur les dernières versions de Node.js.
Note : Récemment dans JSConf, Ryan Dahl, créateur et premier contributeur de Node, a regretté de ne pas s'en tenir à Promises au début de son développement, principalement parce que l'objectif de Node était de créer des serveurs et des fonctions de gestion des fichiers événementiels auxquels le modèle Observer servait mieux.
Conclusion
L'introduction de Promises sur le Web le monde du développement est venu changer la façon dont nous mettons les actions en file d'attente dans notre code et notre façon de raisonner sur l'exécution de notre code et sur la façon dont nous écrivons les bibliothèques et les paquets.
Mais il est plus difficile de sortir des chaînes de rappel que de se résoudre. passer une méthode puis
ne nous ont pas aidés à nous écarter de la pensée après des années d’habitude du modèle Observer et des approches adoptées par les principaux fournisseurs de la communauté comme Node.js.
Nolan Lawson dit dans son excellent article sur les mauvaises utilisations des concaténations Promise les vieilles habitudes de rappel ont la vie dure ! Il expliquera plus tard comment échapper à certains de ces pièges.
Je crois que les promesses étaient nécessaires pour permettre de manière naturelle de générer des tâches asynchrones, mais ne nous aidaient pas beaucoup à progresser sur de meilleurs modèles de code. Nous avons besoin d'une syntaxe linguistique plus adaptable et améliorée.
Nous ne savons toujours pas à quoi ressemblera la spécification ECMAScript dans les années à venir, car nous étendons toujours la gouvernance JavaScript au-delà du Web et tentons de résoudre des énigmes plus complexes.
Il est difficile de dire maintenant ce que au juste. Nous aurons besoin du langage pour que certains de ces puzzles se transforment en programmes plus simples, mais je suis satisfait de la façon dont le Web et JavaScript proprement dit bougent les choses, essayant de s'adapter aux défis et aux nouveaux environnements. Je pense que maintenant, JavaScript est un lieu ami asynchrone plus convivial que lorsque j'ai commencé à écrire du code dans un navigateur il y a plus de dix ans.
Pour en savoir plus

Source link