Fonctions asynchrones: rendre les promesses conviviales

Les fonctions asynchrones vous permettent d'écrire du code basé sur des promesses comme s'il était synchrone.

Jake Archibald
Jake Archibald

Les fonctions asynchrones sont activées par défaut dans Chrome, Edge, Firefox et Safari, et elles sont tout simplement merveilleuses. Ils 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 await une promesse, la fonction est mise en pause de manière non bloquante jusqu'à ce que la promesse soit résolue. Si la promesse est remplie, vous récupérez la valeur. Si la promesse est rejetée, la valeur rejetée est générée.

Prise en charge des navigateurs

Navigateurs pris en charge

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1.

Source

Exemple: journaliser une récupération

Imaginons que vous souhaitiez extraire une URL et consigner 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 la même chose avec 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);
  }
}

Le nombre de lignes est le même, mais tous les rappels ont disparu. Cela facilite la lecture, en particulier pour les personnes moins familières avec les promesses.

Valeurs de retour asynchrones

Les fonctions asynchrones renversent toujours une promesse, que vous utilisiez await ou non. Cette promesse se résout avec ce que la fonction asynchrone renvoie ou rejette avec ce que la fonction asynchrone génère. Avec:

// 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 s'exécute avec "world".

async function foo() {
  await wait(500);
  throw Error('bar');
}

…l'appel de foo() renvoie une promesse qui est rejetée avec Error('bar').

Exemple: Streaming d'une réponse

Les avantages des fonctions asynchrones augmentent dans les exemples plus complexes. Imaginons que vous souhaitiez diffuser une réponse tout en enregistrant les segments et en renvoyant la taille finale.

Voici la version avec des 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-vous comment j'appelle processResult() en lui-même pour configurer une boucle asynchrone ? Un texte qui m'a fait sentir très intelligent. Mais comme la plupart des codes "intelligents", vous devez le regarder pendant des heures pour comprendre ce qu'il fait, comme l'une de ces images à œil magique 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;
}

Tout le "intelligent" a disparu. La boucle asynchrone qui me faisait me sentir si satisfait est remplacée par une boucle while fiable et ennuyeuse. Le résultat est nettement meilleur. À l'avenir, vous obtiendrez des itérateurs asynchrones, qui remplaceront la boucle while par une boucle for-of, ce qui la rendra encore plus soignée.

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 de flèche

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

Méthodes d'objet

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 linéaire

Même si vous écrivez du code qui semble synchrone, veillez à ne pas manquer l'occasion de faire des choses en parallèle.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

L'opération ci-dessus prend 1 000 ms, tandis 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 concret.

Exemple: afficher les récupérations dans l'ordre

Supposons que vous souhaitiez extraire une série d'URL et les consigner dès que possible, dans l'ordre correct.

Inspirez profondément : voici à quoi cela ressemble avec les promesses :

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, c'est bien cela. J'utilise reduce pour enchaîner une séquence de promesses. Je suis tellement intelligent. Mais il s'agit d'un code trop intelligent dont vous pouvez vous passer.

Toutefois, lorsque vous convertissez ce code en fonction asynchrone, il est tentant d'être trop séquentiel:

Non recommandé : trop séquentiel
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Cela semble beaucoup plus propre, mais ma deuxième récupération ne commence pas tant que la première n'a pas été entièrement lue, et ainsi de suite. Cette approche est beaucoup plus lente que l'exemple de promesses qui effectue les récupérations en parallèle. Heureusement, il existe un juste milieu idéal.
Recommandé : bien parallèle
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);
  }
}
Dans cet exemple, les URL sont récupérées et lues en parallèle, mais le bit reduce "intelligent" est remplacé par une boucle for standard, ennuyeuse et lisible.

Solution de contournement pour la prise en charge des navigateurs: générateurs

Si vous ciblez des navigateurs compatibles avec les générateurs (y compris la dernière version de chaque navigateur majeur), vous pouvez polyfiller les fonctions asynchrones.

Babel s'en chargera pour vous. Voici un exemple via le REPL Babel

Je recommande l'approche de transcompilation, car vous pouvez simplement la désactiver une fois que vos navigateurs cibles prennent en charge les fonctions asynchrones. Toutefois, si vous vraiment ne souhaitez pas utiliser de transpileur, vous pouvez utiliser le polyfill de Babel. Au lieu 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. Sinon, le fonctionnement est identique.

Solution de contournement: régénérateur

Si vous ciblez des navigateurs plus anciens, Babel peut également transcompiler des 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 ainsi que du préréglage es2015.

La sortie n'est pas aussi jolie, alors faites attention à l'encombrement du code.

Tout faire de manière asynchrone !

Une fois que les fonctions asynchrones seront disponibles dans tous les navigateurs, utilisez-les pour chaque fonction renvoyant une promesse. Non seulement elles rendent votre code plus clair, mais elles garantissent que la fonction toujours renvoie une promesse.

J'ai été très enthousiaste à propos des fonctions asynchrones en 2014, et c'est formidable de les voir arriver, pour de vrai, dans les navigateurs. Ouais !