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!
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
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.
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
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!
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 :
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!
Toutes les instructions de cette solution sont détaillées dans la vidéo suivante :
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 !