Les fonctions asynchrones vous permettent d'écrire du code basé sur des promesses comme s'il était synchrone.
Les fonctions asynchrones sont activées par défaut dans Chrome, Edge, Firefox et Safari, et elles sont tout à fait remarquables. Elles vous permettent d'écrire du code basé sur des promesses comme s'il était synchrone, mais sans bloquer le thread principal. Ils rendent votre code asynchrone moins "intelligent" et plus lisible.
Les fonctions asynchrones fonctionnent comme suit:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
Si vous utilisez le mot clé async
avant une définition de fonction, vous pouvez ensuite utiliser await
dans la fonction. Lorsque vous utilisez await
une promesse, la fonction est mise en pause de manière non bloquante jusqu'à ce que la promesse soit satisfaite. Si la promesse est tenue, vous obtenez la valeur ajoutée. Si la promesse est refusée, la valeur rejetée est générée.
Prise en charge des navigateurs
Exemple: Consigner une extraction
Imaginons que vous souhaitiez récupérer une URL et enregistrer la réponse sous forme de texte. Voici à quoi cela ressemble avec des promesses:
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
Voici le même résultat lorsque vous utilisez des fonctions asynchrones:
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
} catch (err) {
console.log('fetch failed', err);
}
}
Il s'agit du même nombre de lignes, mais tous les rappels ont disparu. Cela le rend beaucoup plus facile à lire, en particulier pour ceux qui ne connaissent pas bien les promesses.
Valeurs renvoyées asynchrones
Les fonctions asynchrones renvoient toujours une promesse, que vous utilisiez await
ou non. Cette promesse est résolue avec tout ce que la fonction asynchrone renvoie, ou la rejette avec tout ce que la fonction asynchrone renvoie. Voici ce que vous devez faire:
// wait ms milliseconds
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
L'appel de hello()
renvoie une promesse qui satisfait avec "world"
.
async function foo() {
await wait(500);
throw Error('bar');
}
L'appel de foo()
renvoie une promesse qui rejette l'appel avec Error('bar')
.
Exemple: diffuser une réponse en streaming
Les avantages des fonctions asynchrones augmentent avec des exemples plus complexes. Supposons que vous vouliez diffuser une réponse lors de la déconnexion des fragments et renvoyer la taille finale.
Voici les promesses:
function getResponseSize(url) {
return fetch(url).then((response) => {
const reader = response.body.getReader();
let total = 0;
return reader.read().then(function processResult(result) {
if (result.done) return total;
const value = result.value;
total += value.length;
console.log('Received chunk', value);
return reader.read().then(processResult);
});
});
}
Regardez-moi, Jake "porteur de promesses" Archibald. Voyez comment j'appelle processResult()
en lui-même pour configurer une boucle asynchrone. Écrire qui m'a fait me sentir très intelligent. Mais comme la plupart des codes "intelligents", il faut le regarder
pendant des âges pour comprendre ce qu'il fait, comme l'une de ces photos magiques
des années 90.
Réessayons avec des fonctions asynchrones:
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}
return total;
}
Les appareils "intelligents" ont disparu. La boucle asynchrone qui me mettait d'accord
est remplacée par une boucle fluide et ennuyeuse. Le résultat est nettement meilleur. À l'avenir, vous disposerez d'itérateurs asynchrones, qui remplaceraient la boucle while
par une boucle "for-of", ce qui la rend encore plus pratique.
Autre syntaxe de fonction asynchrone
Je vous ai déjà montré async function() {}
, mais le mot clé async
peut être utilisé avec une autre syntaxe de fonction:
Fonctions fléchées
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
Méthodes des objets
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(…);
Méthodes de classe
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);
Attention ! Évitez d'être trop séquentiel
Bien que vous écriviez du code qui semble synchrone, assurez-vous de ne pas manquer l'occasion d'effectuer des tâches en parallèle.
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
L'exécution de la requête ci-dessus prend 1 000 ms, alors que:
async function parallel() {
const wait1 = wait(500); // Start a 500ms timer asynchronously…
const wait2 = wait(500); // …meaning this timer happens in parallel.
await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
return 'done!';
}
L'exécution de l'opération ci-dessus prend 500 ms, car les deux temps d'attente se produisent en même temps. Prenons un exemple pratique.
Exemple: Générer des récupérations dans l'ordre
Imaginons que vous souhaitiez récupérer une série d'URL et les enregistrer dès que possible, dans le bon ordre.
Respiration profonde : voici à quoi cela ressemble :
function markHandled(promise) {
promise.catch(() => {});
return promise;
}
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map((url) => {
return markHandled(fetch(url).then((response) => response.text()));
});
// log them in order
return textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise).then((text) => console.log(text));
}, Promise.resolve());
}
Oui, j'utilise reduce
pour enchaîner une séquence de promesses. Je suis intelligente. Mais il s'agit d'un codage un peu intelligent dont vous avez intérêt à ne pas vous en passer.
Toutefois, lors de la conversion de ce qui précède en une fonction asynchrone, il peut être tentant d'adopter une approche trop séquentielle:
async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } }
function markHandled(...promises) { Promise.allSettled(promises); } async function logInOrder(urls) { // fetch all the URLs in parallel const textPromises = urls.map(async (url) => { const response = await fetch(url); return response.text(); }); markHandled(...textPromises); // log them in sequence for (const textPromise of textPromises) { console.log(await textPromise); } }
Solution de compatibilité du navigateur: générateurs
Si vous ciblez des navigateurs compatibles avec les générateurs (ce qui inclut la dernière version de tous les principaux navigateurs), vous pouvez trier les fonctions asynchrones de polyfill.
Babel s'en chargera pour vous (voici un exemple via le REPL Babel)
- notez à quel point le code transcompilé est similaire. Cette transformation fait partie du préréglage es2017 de Babel.
Je vous recommande l'approche de transpilation, car vous pouvez simplement la désactiver une fois que vos navigateurs cibles prennent en charge les fonctions asynchrones, mais si vous ne voulez vraiment pas utiliser de transpileur, vous pouvez prendre le polyfill de Babel et l'utiliser vous-même. À la place de :
async function slowEcho(val) {
await wait(1000);
return val;
}
vous devez inclure le polyfill et écrire:
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
Notez que vous devez transmettre un générateur (function*
) à createAsyncFunction
et utiliser yield
au lieu de await
. À part cela, cela fonctionne de la même manière.
Solution de contournement: régénérateur
Si vous ciblez des navigateurs plus anciens, Babel peut également transpiler les générateurs, ce qui vous permet d'utiliser des fonctions asynchrones jusqu'à IE8. Pour ce faire, vous avez besoin du préréglage es2017 de Babel et du préréglage es2015 de Babel.
Le résultat n'est pas aussi joli. Faites attention aux surcharges de code.
Tout est asynchrone.
Une fois que les fonctions asynchrones sont disponibles dans tous les navigateurs, utilisez-les sur chaque fonction renvoyant des promesses. Non seulement elles organisent votre code, mais elles permettent aussi de s'assurer que la fonction affichera toujours une promesse.
J'ai été très enthousiaste à propos des fonctions asynchrones en 2014 et je suis ravi de voir qu'elles arrivent réellement dans les navigateurs. Oups !