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 ils sont honnêtement merveilleux. Ils vous permettent d'écrire du code basé sur des promesses s'il était synchrone, mais sans bloquer le thread principal. Ils rendent vos code asynchrone moins "intelligent" et plus lisibles.

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 la commande await pour une promesse, la fonction est mise en pause de manière non bloquante jusqu'à ce que la promesse soit acceptée. Si la promesse est tenue, vous pour en récupérer la valeur. Si la promesse est refusée, la valeur rejetée est générée.

Prise en charge des navigateurs

Navigateurs pris en charge

  • Chrome: 55 <ph type="x-smartling-placeholder">
  • Edge: 15 <ph type="x-smartling-placeholder">
  • Firefox: 52 <ph type="x-smartling-placeholder">
  • Safari: 10.1. <ph type="x-smartling-placeholder">

Source

Exemple: Consigner une extraction

Imaginons que vous souhaitiez récupérer une URL et enregistrer la réponse sous forme de texte. Voici à quoi elle 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 permet plus facile à lire, surtout 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. Cela la promesse est résolue avec tout ce que renvoie la fonction asynchrone, ou rejette avec tout ce que la fonction asynchrone génère. 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. Disons que vous vouliez pour 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. Voir comment j'appelle processResult() en lui-même pour configurer une boucle asynchrone ? Des textes qui ont fait me sentir très intelligente. Comme la plupart des modèles "intelligents", le code, vous devez le regarder pour pour comprendre ce qu'elle fait, comme l'une de ces photos magiques les 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;
}

Toutes les fonctions "intelligentes" a disparu. La boucle asynchrone qui me donnait l'air suffisant est remplacé par une boucle fluide et ennuyeuse. Le résultat est nettement meilleur. À l'avenir, vous bénéficierez itérateurs asynchrones, ce qui Remplacez la boucle while par une boucle For-of pour la rendre encore plus pratique.

Autre syntaxe de fonction asynchrone

Je vous ai déjà montré async function() {}, mais le mot clé async peut être utilisée 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

Même si vous écrivez du code synchrone, veillez à ne pas passer à côté de la possibilité de travailler 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 vouliez récupérer une série d'URL et les enregistrer dès que possible, dans le 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 tellement intelligentes. Mais il s'agit d'un codage un peu intelligent dont vous avez intérêt à ne pas vous en passer.

Cependant, lors de la conversion de ce qui précède en fonction asynchrone, il est tentant trop séquentiel:

Déconseillé (trop séquentiel)
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
<ph type="x-smartling-placeholder"></ph> Cela semble beaucoup plus clair, mais la seconde exploration ne commence que lorsque la première a été entièrement lu, etc. C'est beaucoup plus lent que l'exemple des promesses les extractions en parallèle. Heureusement, il existe un niveau intermédiaire idéal.
Recommandé : sympa et 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);
  }
}
<ph type="x-smartling-placeholder"></ph> Dans cet exemple, les URL sont récupérées et lues en parallèle, mais la version "intelligente" Le bit reduce est remplacé par une boucle "for" standard, ennuyeuse et lisible.

Solution de compatibilité du navigateur: générateurs

Si vous ciblez des navigateurs qui prennent en charge les générateurs (qui incluent 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 la REPL de Babel

Nous vous recommandons d'utiliser la transpilation, car vous pouvez la désactiver navigateurs cibles acceptent les fonctions asynchrones, mais si vous ne voulez vraiment pas utiliser de transpileur, on peut prendre Polyfill de Babel et l'utiliser vous-même. 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 utilisez 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, vous permettant d'utiliser des fonctions asynchrones jusqu'à IE8. Pour ce faire, vous avez besoin Préréglage es2017 de Babel et le préréglage es2015.

Le résultat n'est pas aussi joli. Faites attention et la surcharge de code.

Tout est asynchrone.

Une fois que les fonctions asynchrones sont disponibles dans tous les navigateurs, utilisez-les sur tous les qui renvoie une promesse. Non seulement elles rendent votre code plus clair, mais elles rendent que la fonction renvoie toujours une promesse.

J'étais très enthousiaste à propos des fonctions asynchrones, au cours des 2014, et il est agréable de les voir atterrir, pour de vrai, dans les navigateurs. Oups !