Fermer

juin 2, 2018

Rappels aux promesses d'Async / Await –


JavaScript est régulièrement revendiqué comme étant asynchrone . Qu'est-ce que ça veut dire? Comment cela affecte-t-il le développement? Comment l'approche a-t-elle changé ces dernières années?

Considérons le code suivant:

 result1 = doSomething1 ();
result2 = faire quelque chose2 (résultat1);

La plupart des langues traitent chaque ligne de manière synchrone . La première ligne s'exécute et renvoie un résultat. La deuxième ligne s'exécute une fois que la première est terminée quelle que soit la durée

Single-thread Processing

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. Ceci est nécessaire car les modifications de la page DOM ne peuvent pas se produire sur les threads parallèles; il serait dangereux d'avoir un thread redirigeant vers une URL différente alors qu'un autre essaye d'ajouter des nœuds enfants.

Ceci 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

(Note: d'autres langages comme PHP utilisent aussi un seul thread mais peuvent être gérés par un serveur multithread comme Apache Deux demandes à la même page PHP en même temps peuvent initier deux threads exécutant des instances isolées de l'exécution PHP.)

Going Asynchronous with Callbacks

Les threads uniques 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 pourrait prendre plusieurs secondes – même minutes . Un navigateur deviendrait verrouillé pendant qu'il attendait une réponse. Sur le serveur, une application Node.js ne serait pas capable de traiter d'autres demandes d'utilisateurs.

La ​​solution est un traitement asynchrone. Plutôt que d'attendre l'achèvement, un processus est dit d'appeler une autre fonction lorsque le résultat est prêt. Ceci est connu comme un rappel et il est passé en argument à toute fonction asynchrone. Par exemple:

 doSomethingAsync (callback1);
console.log ('terminé');

// appel quand doSomethingAsync se termine
fonction callback1 (erreur) {
  if (! error) console.log ('doSomethingAsync complete');
}

doSomethingAsync () accepte une fonction de rappel en tant que paramètre (seule une référence à cette fonction est passée, donc il y a peu de frais généraux). Peu importe combien de temps dure doSomethingAsync () ; tout ce que nous savons c'est que callback1 () sera exécuté à un moment donné dans le futur. La console affichera:

 terminé
doSomethingAsync terminé

Callback Hell

Souvent, un rappel n'est jamais appelé que par une 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 ou plusieurs appels asynchrones peut être complétée en série en imbriquant des fonctions de rappel. Par exemple:

 async1 ((err, res) => {
  if (! err) async2 (res, (err, res) => {
    if (! err) async3 (res, (err, res) => {
      console.log ('async1, async2, async3 complete.');
    });
  });
});

Malheureusement, ceci introduit callback hell – un concept notoire que même a sa propre page web ! Le code est difficile à lire, et s'aggravera lorsque la logique de gestion des erreurs sera ajoutée.

Le rappel est relativement rare dans le codage côté client. Il peut aller jusqu'à deux ou trois niveaux de profondeur si vous faites un appel Ajax, en mettant à jour le DOM et en attendant qu'une animation soit terminée, mais cela reste normalement gérable.

La ​​situation est différente sur les processus du système d'exploitation. Un appel 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

Promises

ES2015 (ES6) ] Les rappels sont toujours utilisés sous la surface, mais Promises fournit une syntaxe plus claire qui enchaîne les commandes asynchrones pour qu'elles s'exécutent en série (plus à ce sujet dans la section suivante ). Exécution basée sur la promesse, les fonctions asynchrones de rappel doivent être modifiées afin qu'ils retournent immédiatement un objet Promise. Cet objet promet d'exécuter l'une des deux fonctions (passées comme arguments) à un moment donné dans le futur:

  • resolve : une fonction callback s'exécute quand le traitement se termine avec succès, et
  • rejette [19659027]: une fonction de rappel facultative s'exécute lorsqu'une erreur survient

Dans l'exemple ci-dessous, une API de base de données fournit une méthode connect () qui accepte une fonction de rappel. La fonction externe asyncDBconnect () renvoie immédiatement une nouvelle promesse et exécute soit resolve () soit reject () une fois qu'une connexion est établie ou échoue:

 const db = require ('base de données');

// se connecter à la base de données
Fonction asyncDBconnect (param) {

  return new Promise ((résoudre, rejeter) => {

    db.connect (param, (err, connection) => {
      if (err) rejeter (err);
      d'autre résoudre (connexion);
    });

  });

}

Node.js 8.0+ fournit un utilitaire util.promisify () pour convertir une fonction basée sur le rappel en une alternative basée sur la promesse. Il y a deux conditions:

  1. le callback doit être passé comme dernier paramètre à une fonction asynchrone, et
  2. la fonction callback doit s'attendre à une erreur suivie d'un paramètre value.

Exemple:

 / / Node.js: promisify fs.readFile
const
  util = require ('util'),
  fs = require ('fs'),
  readFileAsync = util.promisify (fs.readFile);

readFileAsync ('file.txt');

Diverses bibliothèques côté client fournissent également des options de promisify, mais vous pouvez en créer une vous-même en quelques lignes:

 // promisify une fonction de rappel passée comme dernier paramètre
// la fonction de rappel doit accepter les paramètres (err, data)
fonction promisify (fn) {
  return function () {
      retourner une nouvelle promesse (
        (résoudre, rejeter) => fn (
          ... Array.from (arguments),
        (err, data) => err? rejeter (err): résoudre (données)
      )
    )
  }
}

// Exemple
fonction wait (heure, rappel) {
  setTimeout (() => {callback (null, 'done');}, heure);
}

const asyncWait = promisify (attendre);

ayscWait (1000);

Chaînage asynchrone

Tout ce qui renvoie une promesse peut lancer une série d'appels de fonctions asynchrones définis dans les méthodes .then () . On obtient le résultat de la résolution précédente :

 asyncDBconnect ('http: // localhost: 1234')
  .then (asyncGetSession) // passe le résultat de asyncDBconnect
  .then (asyncGetUser) // passe le résultat de asyncGetSession
  .then (asyncLogAccess) // passe le résultat de asyncGetUser
  .then (résultat => {// fonction non-asynchrone
    console.log ('complete'); // (résultat passé de asyncLogAccess)
    résultat de retour; // (résultat passé au suivant .then ())
  })
  .catch (err => {// appelé sur un rejet
    console.log ('erreur', err);
  });

Les fonctions synchrones peuvent également être exécutées dans .then () blocs. La valeur renvoyée est transmise à la suivante .then () (le cas échéant).

La ​​méthode .catch () définit une fonction qui est appelée quand un rejet précédent est viré. À ce stade, aucune autre méthode .then () ne sera appliquée. Vous pouvez avoir plusieurs .catch () méthodes tout au long de la chaîne pour capturer différentes erreurs.

ES2018 introduit une méthode .finally () qui exécute toute logique finale quel que soit le résultat - par exemple, pour nettoyer, fermer une connexion à une base de données, etc. Il est actuellement supporté dans Chrome et Firefox seulement, mais le comité technique 39 a publié un .finally () polyfill .

 function doSomething () {
  faire quelque chose1 ()
  .then (doSomething2)
  .then (doSomething3)
  .catch (err => {
    console.log (err);
  })
  .finally (() => {
    // ranger-ici!
  });
}

Plusieurs appels asynchrones avec Promise.all ()

Promise .then () Les méthodes exécutent des fonctions asynchrones les unes après les autres. Si l'ordre n'a pas d'importance - par exemple, l'initialisation de composants non liés - il est plus rapide de lancer toutes les fonctions asynchrones en même temps et de terminer lorsque la dernière fonction (la plus lente) est exécutée . réalisé avec Promise.all () . Il accepte un ensemble de fonctions et renvoie une autre promesse. Par exemple:

 Promise.all ([ async1, async2, async3 ])
  .then (values ​​=> {// tableau de valeurs résolues
    console.log (valeurs); // (dans le même ordre que le tableau de fonctions)
    renvoyer des valeurs;
  })
  .catch (err => {// appelé sur un rejet
    console.log ('erreur', err);
  });

Promise.all () se termine immédiatement si l'une des fonctions asynchrones appelle rejeter .

Plusieurs appels asynchrones avec Promise.race ()

Promise.race () est similaire à Promise.all () sauf qu'elle sera résolue ou rejetée dès que la première promesse aura été résolue ou rejetée. Seule la fonction asynchrone basée sur Promise la plus rapide se terminera jamais:

 Promise.race ([ async1, async2, async3 ])
  .then (valeur => {// valeur unique
    console.log (valeur);
    valeur de retour;
  })
  .catch (err => {// appelé sur un rejet
    console.log ('erreur', err);
  });

Un avenir prometteur

Les promesses réduisent l'enfer rappel mais introduisent leurs propres problèmes.

Les didacticiels omettent souvent de mentionner que la chaîne Promise entière est asynchrone . N'importe quelle fonction utilisant une série de promesses devrait renvoyer sa propre promesse ou exécuter des fonctions de rappel dans le final .then () .catch () ou .finally () méthodes

J'ai aussi une confession: Les promesses m'ont longtemps embrouillé . 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.

Autres ressources de promesse:

Async / Await

Les promesses peuvent être décourageantes, alors ES2017 introduit async et attendre . Bien qu'il ne s'agisse que de sucre syntaxique, il rend les Promesses beaucoup plus douces, et vous pouvez complètement éviter les chaînes. Considérons l'exemple basé sur la promesse ci-dessous:

 function connect () {

  return new Promise ((résoudre, rejeter) => {

    asyncDBconnect ('http: // localhost: 1234')
      .then (asyncGetSession)
      .then (asyncGetUser)
      .then (asyncLogAccess)
      .then (résultat => résolution (résultat))
      .catch (err => rejeter (err))

  });
}

// run connect (fonction auto-exécutable)
(() => {
  relier();
    .then (résultat => console.log (résultat))
    .catch (err => console.log (err))
}) ();

Pour réécrire cela en utilisant async / await :

  1. la fonction externe doit être précédée d'une instruction async et
  2. appelle asynchrone Les fonctions basées sur la promesse doivent être précédées de await pour s'assurer que le traitement est terminé avant l'exécution de la prochaine commande.
 async function connect () {

  essayez {
    const
      connection = wait asyncDBconnect ('http: // localhost: 1234'),
      session = wait asyncGetSession (connexion),
      user = wait asyncGetUser (session),
      log = wait asyncLogAccess (utilisateur);

    retour journal;
  }
  attraper (e) {
    console.log ('erreur', err);
    return null;
  }

}

// run connect (fonction asynchrone auto-exécutable)
(async () => {await connect ();}) ();

await fait effectivement apparaître chaque appel comme s'il était synchrone, sans tenir le thread de traitement unique de JavaScript. De plus, les fonctions asynchrones renvoient toujours une Promesse pour qu'elles puissent, à leur tour, être appelées par d'autres fonctions asynchrones .

async / code ne peut pas être plus court, mais il y a des avantages considérables:

  1. La syntaxe est plus propre. Il y a moins de parenthèses et moins de choses à se tromper.
  2. Le débogage est plus facile. Les points d'arrêt peuvent être définis sur n'importe quelle instruction await .
  3. La gestion des erreurs est meilleure. try / catch les blocs peuvent être utilisés de la même manière qu'un code synchrone.
  4. Le support est bon. Il est implémenté dans tous les navigateurs (sauf IE et Opera Mini) et Node 7.6 +

Cela étant dit, tout n'est pas parfait ...

Promesses, promesses

async / await s'appuie toujours sur Promises, qui dépendent finalement des rappels. Vous aurez besoin de comprendre comment fonctionne Promises, et il n'y a aucun équivalent direct de Promise.all () et Promise.race () . Il est facile d'oublier Promise.all () qui est plus efficace que d'utiliser une série de commandes d'attente sans lien

Asynchronous attend dans les boucles synchrones

vous essayerez d'appeler une fonction asynchrone dans une boucle synchrone. Par exemple:

 async function process (array) {
  pour (laissez-moi de tableau) {
    attendre quelque chose (i);
  }
}

Ça ne marchera pas. Cela non plus:

 async function process (array) {
  array.forEach (async i => {
    attendre quelque chose (i);
  });
}

Les boucles elles-mêmes restent synchrones et finiront toujours avant leurs opérations asynchrones internes

ES2018 introduit des itérateurs asynchrones, qui sont comme des itérateurs réguliers, sauf que la méthode next () renvoie une promesse. Par conséquent, le mot-clé await peut être utilisé avec pour ... sur boucles pour exécuter des opérations asynchrones en série. par exemple:

 async function process (array) {
  pour attendre (let i of array) {
    faire quelque chose (i);
  }
}

Cependant, jusqu'à ce que des itérateurs asynchrones soient implémentés, il vaut peut-être mieux mapper des éléments de tableau à une fonction async et les exécuter avec Promise.all () . Par exemple:

 const
  todo = ['a', 'b', 'c'],
  alltodo = todo.map (async (v, i) => {
    console.log ('itération', i);
    attendre quelque chose (v);
});

attendez Promise.all (alltodo);

Cela a l'avantage d'exécuter des tâches en parallèle, mais il n'est pas possible de passer le résultat d'une itération à une autre, et le mappage de grands tableaux peut être coûteux en ressources. quitte silencieusement si vous omettez un essai / attrape autour de attente qui échoue. Si vous avez un long ensemble de commandes wait asynchrones, vous pouvez avoir besoin de plusieurs try / catch blocs.

Une alternative est une fonction d'ordre supérieur, qui attrape les erreurs try / catch les blocs deviennent inutiles (merci à @wesbos pour la suggestion):

 async function connect () {

  const
    connection = wait asyncDBconnect ('http: // localhost: 1234'),
    session = wait asyncGetSession (connexion),
    user = wait asyncGetUser (session),
    log = wait asyncLogAccess (utilisateur);

  retourner vrai;
}

// fonction d'ordre supérieur pour intercepter les erreurs
function catchErrors (fn) {
  Fonction de retour (... args) {
    return fn (... args) .catch (err => {
      console.log ('ERROR', err);
    });
  }
}

(async () => {
  attendez catchErrors (connect) ();
}) ();

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.

Malgré quelques pièges, async / attend est un ajout élégant à JavaScript. Autres ressources:

JavaScript Journey

La programmation asynchrone est un défi qu'il est impossible d'éviter en JavaScript. Les rappels sont essentiels dans la plupart des applications, mais il est facile de s'enchevêtrer dans des fonctions profondément imbriquées.

Promet des rappels abstraits, mais il existe de nombreux pièges syntaxiques. La conversion de fonctions existantes peut être une corvée et les chaînes .then () semblent encore 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 la façon dont vous écrivez JavaScript et pourrait même vous faire apprécier Promises – si vous ne l'avez pas fait auparavant!






Source link