Contrôle de flux en JavaScript : rappels, promesses, async/attente

Dans cet article, nous examinerons de manière approfondie comment travailler avec du code asynchrone en JavaScript. Nous commencerons par les rappels, passerons aux promesses, puis terminerons par les plus modernes async/await
. Chaque section proposera un exemple de code, décrira les principaux points à connaître et établira un lien vers des ressources plus approfondies.
Contenu:
- Traitement à un seul thread
- Devenir asynchrone avec des rappels
- Promesses
- asynchrone/attendre
- Voyage JavaScript
JavaScript est régulièrement revendiqué comme étant asynchrone. Qu’est-ce que cela signifie? Comment affecte-t-il le développement ? Comment l’approche a-t-elle évolué ces dernières années ?
Considérez le code suivant :
result1 = doSomething1();
result2 = doSomething2(result1);
La plupart des langages traitent chaque ligne de manière synchrone. La première ligne s’exécute et renvoie un résultat. La deuxième ligne fonctionne une fois la première terminée — peu importe le temps qu’il faut.
Traitement à un seul thread
JavaScript s’exécute sur un seul thread de traitement. Lors de l’exécution dans un onglet du navigateur, tout le reste s’arrête. Cela est nécessaire car les modifications apportées au DOM de la page ne peuvent pas se produire sur les threads parallèles ; il serait dangereux qu’un thread redirige vers une URL différente tandis qu’un autre tente d’ajouter des nœuds enfants.
Cela est rarement évident pour l’utilisateur, car le traitement se produit rapidement en petits morceaux. Par exemple, JavaScript détecte un clic sur un bouton, exécute un calcul et met à jour le DOM. Une fois terminé, le navigateur est libre de traiter l’élément suivant dans la file d’attente.
(Remarque : d’autres langages tels que PHP utilisent également un seul thread, mais peuvent être gérés par un serveur multi-thread tel qu’Apache. Deux requêtes vers la même page PHP en même temps peuvent lancer deux threads exécutant des instances isolées du runtime PHP. .)
Devenir asynchrone avec des rappels
Les threads simples posent un problème. Que se passe-t-il lorsque JavaScript appelle un processus « lent » tel qu’une requête Ajax dans le navigateur ou une opération de base de données sur le serveur ? Cette opération peut prendre plusieurs secondes — quelques minutes. Un navigateur se verrouille alors qu’il attend une réponse. Sur le serveur, une application Node.js ne serait pas en mesure de traiter d’autres demandes d’utilisateurs.
La solution est le traitement asynchrone. Plutôt que attendre la fin, un processus est invité à appeler une autre fonction lorsque le résultat est prêt. Ceci est connu comme un rappeleret il est passé en argument à n’importe quelle fonction asynchrone.
Par example:
doSomethingAsync(callback1);
console.log('finished');
function callback1(error) {
if (!error) console.log('doSomethingAsync complete');
}
Les doSomethingAsync
fonction accepte un rappel en tant que paramètre (seule une référence à cette fonction est transmise, il y a donc peu de surcharge). Peu importe combien de temps doSomethingAsync
prend; tout ce que nous savons c’est que callback1
sera exécuté à un moment donné dans le futur. La console affichera ceci :
finished
doSomethingAsync complete
Vous pouvez en savoir plus sur les rappels dans Retour aux fondamentaux : que sont les rappels en JavaScript ?
L’enfer des rappels
Souvent, un rappel n’est appelé que par une seule fonction asynchrone. Il est donc possible d’utiliser des fonctions en ligne concises et anonymes :
doSomethingAsync(error => {
if (!error) console.log('doSomethingAsync complete');
});
Une série de deux appels asynchrones ou plus peut être effectuée en série en imbriquant des fonctions de rappel. Par example:
async1((err, res) => {
if (!err) async2(res, (err, res) => {
if (!err) async3(res, (err, res) => {
console.log('async1, async2, async3 complete.');
});
});
});
Malheureusement, cela introduit enfer de rappel — un concept notoire que même a sa propre page web! Le code est difficile à lire et s’aggravera lorsqu’une logique de gestion des erreurs sera ajoutée.
L’enfer du rappel est relativement rare dans le codage côté client. Cela peut aller jusqu’à deux ou trois niveaux si vous effectuez un appel Ajax, mettez à jour le DOM et attendez qu’une animation se termine, mais cela reste normalement gérable.
La situation est différente pour les processus du système d’exploitation ou du serveur. Un appel d’API Node.js peut recevoir des téléchargements de fichiers, mettre à jour plusieurs tables de base de données, écrire dans des journaux et effectuer d’autres appels d’API avant qu’une réponse puisse être envoyée.
Vous pouvez en savoir plus sur l’enfer du rappel dans Sauvé de l’enfer des rappels.
Promesses
ES2015 (ES6) a introduit des promesses. Les rappels sont toujours utilisés sous la surface, mais les promesses fournissent une syntaxe plus claire qui Chaînes commandes asynchrones afin qu’elles s’exécutent en série (plus d’informations à ce sujet dans le section suivante).
Pour activer l’exécution basée sur la promesse, les fonctions basées sur le rappel asynchrone doivent être modifiées afin qu’elles renvoient immédiatement un objet de promesse. Cet objet promesses pour exécuter l’une des deux fonctions (passées en arguments) à un moment donné dans le futur :
resolve
: une fonction de rappel exécutée lorsque le traitement se termine avec succèsreject
: une fonction de rappel optionnelle exécutée lorsqu’une panne survient
Dans l’exemple ci-dessous, une API de base de données fournit un connect
méthode qui accepte une fonction de rappel. L’extérieur asyncDBconnect
La fonction renvoie immédiatement une nouvelle promesse et exécute soit resolve
ou alors reject
une fois qu’une connexion est établie ou échoue :
const db = require('database');
function asyncDBconnect(param) {
return new Promise((resolve, reject) => {
db.connect(param, (err, connection) => {
if (err) reject(err);
else resolve(connection);
});
});
}
Node.js 8.0+ fournit un utilitaire util.promisify() pour convertir une fonction basée sur un rappel en une alternative basée sur une promesse. Il y a quelques conditions :
- le rappel doit être passé comme dernier paramètre à une fonction asynchrone
- la fonction de rappel doit attendre une erreur suivie d’un paramètre de valeur
Exemple:
const
util = require('util'),
fs = require('fs'),
readFileAsync = util.promisify(fs.readFile);
readFileAsync('file.txt');
Chaînage asynchrone
Tout ce qui renvoie une promesse peut démarrer une série d’appels de fonction asynchrones définis dans .then()
méthodes. Chacun reçoit le résultat du précédent resolve
:
asyncDBconnect('http://localhost:1234')
.then(asyncGetSession)
.then(asyncGetUser)
.then(asyncLogAccess)
.then(result => {
console.log('complete');
return result;
})
.catch(err => {
console.log('error', err);
});
Les fonctions synchrones peuvent également être exécutées dans .then()
blocs. La valeur retournée est passée au suivant .then()
(si seulement).
Les .catch()
définit une fonction qui est appelée lors d’une précédente reject
est congédié. A ce moment-là, pas plus .then()
méthodes seront exécutées. Vous pouvez avoir plusieurs .catch()
méthodes tout au long de la chaîne pour capturer différentes erreurs.
ES2018 introduit une .finally()
, qui exécute n’importe quelle logique finale quel que soit le résultat – par exemple, pour nettoyer, fermer une connexion à la base de données, etc. Il est pris en charge dans tous les navigateurs modernes:
function doSomething() {
doSomething1()
.then(doSomething2)
.then(doSomething3)
.catch(err => {
console.log(err);
})
.finally(() => {
});
}
Un avenir prometteur ?
Les promesses réduisent l’enfer des rappels mais introduisent leurs propres problèmes.
Les tutoriels omettent souvent de mentionner que toute la chaîne de promesse est asynchrone. Toute fonction utilisant une série de promesses doit soit renvoyer sa propre promesse, soit exécuter des fonctions de rappel dans la version finale. .then()
, .catch()
ou alors .finally()
méthodes.
J’ai aussi un aveu : les promesses m’ont longtemps dérouté. La syntaxe semble souvent plus compliquée que les rappels, il y a beaucoup à se tromper et le débogage peut être problématique. Cependant, il est essentiel d’apprendre les bases.
Vous pouvez en savoir plus sur les promesses dans Un aperçu des promesses JavaScript.
asynchrone/attendre
Les promesses peuvent être intimidantes, alors ES2017 introduit async
et await
. Bien qu’il ne s’agisse que de sucre syntaxique, il rend les promesses beaucoup plus douces et vous pouvez éviter .then()
chaînes tout à fait. Considérez l’exemple basé sur les promesses ci-dessous :
function connect() {
return new Promise((resolve, reject) => {
asyncDBconnect('http://localhost:1234')
.then(asyncGetSession)
.then(asyncGetUser)
.then(asyncLogAccess)
.then(result => resolve(result))
.catch(err => reject(err))
});
}
(() => {
connect();
.then(result => console.log(result))
.catch(err => console.log(err))
})();
Pour réécrire cela en utilisant async/await
:
- la fonction externe doit être précédée d’un
async
déclaration - les appels aux fonctions asynchrones basées sur des promesses doivent être précédés de
await
pour s’assurer que le traitement est terminé avant l’exécution de la commande suivante
async function connect() {
try {
const
connection = await asyncDBconnect('http://localhost:1234'),
session = await asyncGetSession(connection),
user = await asyncGetUser(session),
log = await asyncLogAccess(user);
return log;
}
catch (e) {
console.log('error', err);
return null;
}
}
(async () => { await connect(); })();
await
fait en sorte que chaque appel apparaisse comme s’il était synchrone, sans retarder le thread de traitement unique de JavaScript. En outre, async
les fonctions renvoient toujours une promesse afin qu’elles puissent à leur tour être appelées par d’autres async
les fonctions.
async/await
code n’est peut-être pas plus court, mais il y a des avantages considérables :
- La syntaxe est plus propre. Il y a moins de parenthèses et moins de possibilités de se tromper.
- Le débogage est plus facile. Les points d’arrêt peuvent être définis sur n’importe quel
await
déclaration. - La gestion des erreurs est meilleure.
try/catch
les blocs peuvent être utilisés de la même manière que le code synchrone. - Le soutien est bon. Il est implémenté dans tous les navigateurs modernes et nœud 7.6+.
Cela dit, tout n’est pas parfait…
Promesses, promesses
async/await
repose sur des promesses, qui reposent finalement sur des rappels. Cela signifie que vous devrez toujours comprendre comment fonctionnent les promesses.
De plus, lorsque vous travaillez avec plusieurs opérations asynchrones, il n’y a pas d’équivalent direct de Promesse.tout ou alors Promesse.race. C’est facile d’oublier Promise.all
ce qui est plus efficace que d’utiliser une série de await
commandes.
essayer/attraper la laideur
async
les fonctions se fermeront silencieusement si vous omettez un try/catch
autour de n’importe quel await
qui échoue. Si vous avez un long ensemble de await
commandes, vous aurez peut-être besoin de plusieurs try/catch
blocs.
Une alternative est un fonction d’ordre supérieur, qui détecte les erreurs pour que try/catch
les blocs deviennent inutiles (merci à @wesbos pour la suggestion).
Cependant, cette option peut ne pas être pratique dans les situations où une application doit réagir à certaines erreurs d’une manière différente des autres.
Pourtant, malgré quelques écueils, async/await
est un ajout élégant à JavaScript.
Vous pouvez en savoir plus sur l’utilisation async/await
dans Guide du débutant sur JavaScript async/wait, avec des exemples.
Voyage JavaScript
La programmation asynchrone est un défi impossible à éviter en JavaScript. Les rappels sont essentiels dans la plupart des applications, mais il est facile de s’emmêler dans des fonctions profondément imbriquées.
Les promesses font abstraction des rappels, mais il existe de nombreux pièges syntaxiques. La conversion de fonctions existantes peut être une corvée et .then()
les chaînes ont toujours l’air désordonnées.
Heureusement, async/await
apporte de la clarté. Le code semble synchrone, mais il ne peut pas monopoliser le thread de traitement unique. Cela changera votre façon d’écrire JavaScript et pourrait même vous faire apprécier les promesses — si vous ne le faisiez pas avant !
Source link