Programmation asynchrone en JavaScript

blog-thumb

Introduction

Aujourd’hui, il est clair que l’écosystème JavaScript est incontournable pour le développement d’applications web et mobiles modernes (frontend et backend). La plupart des frameworks frontend modernes sont en javaScript tels que ReactJs , Angular , VueJs etc… C’est aussi le même constat pour les frameworks, outils et lib d’automatisation de tests. Les frameworks comme PlayWright , WebdriverIO , Cypress , NightwatchJs sont tous en JavaScript.

Avant d’entrer dans le vif du sujet, il est important de rappeler que JavaScript est un langage mono-thread et synchrone.

Un seul thread signifie que le code JavaScript n’a qu’un seul thread principal pour exécuter toutes les instructions (seul et unique thread pour toute son exécution)

Le thread est non bloquant lorsque le code doit effectuer une tâche asynchrone (une tâche qui ne peut pas renvoyer le résultat immédiatement et prend un certain temps pour revenir, comme un événement d’E/S), le thread principal suspendra la tâche, puis exécutera le callback correspondant selon certaines règles lorsque la tâche asynchrone retourne le résultat.

Si quelqu’un vous dit le contraire, je vous propose de lui demander de bien vouloir regarder cette vidéo de Philip Roberts pour mieux comprendre le fonctionnement de JavaScript et plus particulièrement l’event loop!

Code synchrone vs code asynchrone

Exemple de programme synchrone

Examinons l’exemple suivant:

console.log("Before");
console.log("Welcome");
console.log("After");

Dans ce bloc, les lignes sont exécutées les unes après les autres. Pendant que chaque instruction est en cours de traitement, rien d’autre ne peut se produire. Tout le reste est bloqué jusqu’à la fin d’exécution de l’opération / instruction en cours. Cela est dû au fait que, JavaScript est ne dispose que d’un seul thread.

Before
Welcome
After

Exemple de programme asynchrone

Les fonctions JavaScript sont synchrone, les accès I/O sont asynchrones (accès fichiers, accès base de données, requêtes HTTP, …). Prenons comme exemple la fonction “setTimeout(callback, timeout)” qui est une fonction asynchrone qui exécute une fonction donnée (callback) après un délai exprimé en millisecondes.

Examinons l’exemple suivant:

console.log("Before");
setTimeout(() => {
  console.log("Reading a user from a database ...");
}, 2000);
console.log("After");

Dans ce bloc, la méthode setTimeout est exécutée de façon asynchrone sans bloquer le thread principal. On voit que la réponse de la méthode setTimeout (Reading a user from a database ...) arrive après 2 secondes d’attente sans bloquer le thread ni l’exécution des autres instructions…

Before
After
Reading a user from a database ...

Maintenant je vous invite à regarder cette vidéo sur ma chaîne YouTube pour en savoir plus sur la différence entre la programmation synchrone et asynchrone en JavaScript.

Vidéo [1/4] Synchrone vs Asynchrone

Les callbacks & l’enfer des callbacks (Callback hell)

Un callback ou fonction de rappel/retour est une fonction passée dans une autre fonction en tant qu’argument, qui est ensuite invoquée à l’intérieur de la fonction externe pour accomplir une tâche bien précise

function sayHelloTo(msg, callback){
  var message = "Hello, Welcome to " + msg;
  callback(message);
}
function displayWelcomeMsg(msg){
  console.log(msg);
}
sayHelloTo("ExpandTesting.com!", displayWelcomeMsg);
Hello, Welcome to ExpandTesting.com!

Dans l’exemple ci-dessus, sayHelloTo est la fonction externe d’ordre supérieur, qui accepte deux arguments, le premier c’est le message et le second c’est le callback. La fonction displayWelcomeMsg est transmise en tant que fonction callback qui est ensuite invoquée à l’intérieur de la fonction sayHelloTo

Nous pouvons aussi transmettre des fonctions anonymes en tant que callback. L’appel ci-dessous à la fonction sayHelloTo aurait le même résultat que l’exemple ci-dessus:

sayHelloTo("ExpandTesting.com!", function(msg){
  console.log(msg);
});

C’est aussi le même principe avec les fonctions fléchées

sayHelloTo("ExpandTesting.com!", (msg) => {
  console.log(msg);
});

Prenons un exemple illustratif:

Supposons que nous devons récupérer les messages de “commit” pour un repo github d’un utilisateur donné. Nous allons avoir une première méthode getUser qui va chercher l’utilisateur correspondant à un identifiant. Une fois l’utilisateur trouvé, il faut appeler la fonction asynchrone getRepos qui va renvoyer les repos de l’utilisateur, une fois les repos obtenus, il faut appeler la fonction asynchrone getCommits qui va récupérer les messages de “commit” pour un repo donné via les API Github.

Nous allons maintenant développer une première solution avec l’utilisation des callbacks.

function getCommits(repoName, callback) {
  setTimeout(() => {
    console.log(`Calling Github API for ${repoName}...`);
    callback(["commit 1", "commit 2"]);
  }, 2000);
}

function getRepos(userName, callback) {
  setTimeout(() => {
    console.log(`Calling Github API for ${userName}...`);
    callback(["repo1", "repo2"]);
  }, 2000);
}

function getUser(id, callback) {
  setTimeout(() => {
    console.log("Reading a user from a database ...");
    callback({ id: id, name: "tawfiknouri" });
  }, 2000);
}

getUser(1, (user) => {
  getRepos(user.name, (repos) => {
    getCommits(repos[0], (commits) => {
      console.log(commits)
    })
  })
});
Reading a user from a database ...
Calling Github API for tawfiknouri...
Calling Github API for repo1...
[ 'commit 1', 'commit 2' ]

L’imbrication de plusieurs appels asynchrones en passant les callbacks en cas de succès et en cas d’erreur, sans parler des cas où il faut s’assurer que tous les appels à des fonctions asynchrones exécutées en parallèles soient terminés, devient très vite infernal on parle alors de callbacks hell.

Toutes les instructions de cette solution sont détaillées dans la vidéo suivante

Vidéo [2/4] Les callbacks

Promises

Une promesse est un objet de type Promise qui permet de simplifier l’écriture des fonctions asynchrones. Un objet de type Promise est instancié en lui fournissant en argument la fonction asynchrone à exécuter qui prend elle-même en arguments la fonction callback à exécuter en cas de succès (resolve) et celle à exécuter en cas d’erreur (reject).

Voici un exemple de déclaration et d’utilisation d’un objet Promise:

const p = new Promise((resolve, reject)=> {

    // kick off some async work
    setTimeout(() => {
        const flag = true;
        if (flag === true)
        {
            resolve("Time is up!");
        }
        else
        {
            reject("Oops!");
        }

    }, 2000);

});

p.then( (msg) => {console.log(msg)})
 .catch ((msg) => {console.log(msg)})
 .finally ( () => {
     console.log("Always called!");
 });
Time is up!
Always called!
  • La méthode then() de l’objet promise p permet d’exploiter le résultat de la fonction asynchrone.
  • La méthode catch() de l’objet promise p permet la gestion des erreurs qui peuvent survenir lors de l’exécution de la fonction asynchrone.
  • La méthode finally() de l’objet promise p permet d’exécuter un traitement dès qu’une promesse est terminée, qu’elle soit tenue ou rejetée

Repérons maintenant notre exemple initial et le redéveloppé en utilisant les Promises:

function getCommits(repoName) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Calling Github API for ${repoName}...`);
      resolve(["commit 1", "commit 2"]);
    }, 2000);
  });
}

function getRepos(userName) {
  return new Promise((resolve, rject) => {
    setTimeout(() => {
      console.log(`Calling Github API for ${userName}...`);
      resolve(["repo1", "repo2"]);
    }, 2000);
  });
}

function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Reading a user from a database ...");
      resolve({ id: id, name: "tawfiknouri" });
    }, 2000);
  });
}

getUser(1)
  .then((user) => getRepos(user.name))
  .then((repos) => getCommits(repos[0]))
  .then((commits) => {
    console.log(commits);
  })
  .catch((err) => {
    console.log(err);
  })
  .finally(() => {
    console.log("Done!");
  });
Reading a user from a database ...
Calling Github API for tawfiknouri...
Calling Github API for repo1...
[ 'commit 1', 'commit 2' ]
Done!

Toutes les instructions de cette solution sont détaillées dans la vidéo suivante :

Vidéo [3/4] Les promises

Async and Await

ECMAScript 7 (ES7) a introduit deux nouveaux mots-clefs (async et await) permettant de simplifier davantage l’écriture de fonctions asynchrones en JavaScript pour qu’elles ressemblent aux fonctions synchrones.

La déclaration d’une fonction asynchrone doit être précédée de mot-clef async, tandis que l’appel à une fonction asynchrone est, quant à elle, précédée de mot-clef await.

Le mot-clef async créera automatiquement une promesse qu’elle va résoudre avec le résultat du retour.

Le mot-clef await attendra que la promesse soit résolue et qu’elle retourne un résultat, il ne peut être placé que dans une fonction elle-même asynchrone !

Repérons maintenant notre exemple initial à l’aide de la syntaxe d’async/await:

async function displayCommits() {
  try {
    const user = await getUser(1);
    const repos = await getRepos(user.name);
    const commits = await getCommits(repos[0]);
    console.log(commits);
  } catch (error) {
    console.log(error);
  }
  finally {
    console.log("Done!");
  }
}
displayCommits();
Reading a user from a database ...
Calling Github API for tawfiknouri...
Calling Github API for repo1...
[ 'commit 1', 'commit 2' ]
Done!
  • Le bloc try exécutera un code sensible qui peut lever des exceptions
  • Le bloc catch sera utilisé chaque fois qu’une exception (du type pris) est levée dans le bloc try
  • Le bloc finally est appelé dans chaque cas après les blocs try/catch.

Toutes les instructions de cette solution sont détaillées dans la vidéo suivante :

Vidéo [4/4] La syntaxe Async / Await

Si vous avez aimé cet article, n’hésitez pas à le partager ! J’espère que vous allez apprécier les vidéos, n’oubliez pas de vous abonner à ma chaîne YouTube , merci pour votre support !

comments powered by Disqus