Plan du site  
pixel
pixel

Articles - Étudiants SUPINFO

Programmation Asynchrone en JavaScript

Par Joris REBOUILLAT Publié le 24/09/2017 à 19:20:39 Noter cet article:
(0 votes)
Avis favorable du comité de lecture

Introduction

En JavaScript, beaucoup d’opérations ont un fonctionnement dit "asynchrone", c’est-à-dire un fonctionnement non bloquant qui va nous permettre de lancer une tâche et d’avancer dans le programme le temps que cette tâche s’exécute (ex : lorsque l’on manipule des éléments HTML ou que l’on fait des requêtes AJAX, la page n’est pas bloquée).

Voici un exemple :

console.log("Premier")

setTimeout(function () {
    console.log("Deuxième")
}, 1000);

console.log("Troisième")

// ==> Résultat:
// Premier
// Troisième
// Deuxième

La fonction "setTimeout" prend deux paramètres : une fonction (appelée callback) et un entier qui est le nombre de millisecondes à attendre avant d’appeler le callback. Étant donné que "setTimeout" est asynchrone, le script va continuer son déroulement jusqu’à ce que le callback soit appelé.

Les callbacks

Un callback est une fonction de rappel passée en paramètre d’une autre fonction et qui sera appelée à la fin de cette dernière pour effectuer une autre opération. Plus clairement, utiliser un callback se résume à dire "j’exécute la fonction B une fois que la fonction A est terminée".

Un petit exemple pour illustrer ce procédé :

function helloWorld() {
    console.log("Hello World !");
}

function setup(soft, callBack) {
    console.log("Installation de " + soft);
    callBack()
}

setup("Nginx", helloWorld)

// ==> Résultat:
// Installation de Nginx
// Hello World !

Ici la fonction "helloWorld" est passée en paramètre de la fonction "setup", c’est bien un callback. Les callbacks sont beaucoup utilisés pour faire des requêtes HTTP, pour traiter des fichiers (avec NodeJS) ou encore dans la déclaration des événements, comme le montre cet exemple :

document.getElementById("monBouton").addEventListener("click", function() {
    alert("Vous avez cliqué sur le bouton !")
});

Depuis la 6e version du standard EcmaScript (2015), une nouvelle syntaxe a été mise à disposition pour définir une fonction, elle s’appelle la fonction fléchée.

Elle se construit sous cette forme :

myFunction = () => {
    console.log("Fonction fléchée !")
}

Les fonctions fléchées sont principalement utilisées comme des callbacks.

Si l’on reprend l’exemple précédent avec cette syntaxe plus simpliste, cela nous donne:

document.getElementById("monBouton").addEventListener("click", () => {
    alert("Vous avez cliqué sur le bouton !")
});

Les Promises

L’objet "Promise", standardisé depuis EcmaScript 6 (2015), permet de faire des opérations asynchrones plus simplement. Son fonctionnement est simple : on lance une opération de manière asynchrone dont on attend un résultat dans le futur (promesse, comme son nom l’indique).

Elle se construit de cette manière :

new Promise(function(resolve, reject) {
    setTimeout(function () {
        resolve("Promise résolue")
    }, 1000);
});

La Promise prend en paramètre une fonction qu’elle exécute immédiatement et qui prend elle même deux paramètres : une fonction de callback qui permet de signaler que la promesse a été tenue (tout s’est bien passé donc on retourne le résultat) et une deuxième fonction de callback qui permet cette fois de signaler que la promesse est en échec (possibilité de retourner une erreur).

Un avantage de la Promise est qu’elle peut être dans 3 états différents :

  • Pending (en attente): l’opération est en cours d’exécution.

  • Fulfilled (satisfaite) : l’opération est terminée et a réussie.

  • Rejected (rejetée) : l’opération est terminée et a échouée.

On peut donc effectuer une tâche asynchrone, attendre la fin de celle-ci sans bloquer le fil d’exécution du processus puis continuer le déroulement de notre script.

Voici un exemple montrant comment traiter la donnée d’une requête HTTP

function getHTTP(url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.onload = function() {
            if (this.status >= 200 && this.status < 300) {
                resolve(xhr.response);
            }
        };
        xhr.open("GET", url);
        xhr.send();
    });
}

getHTTP("https://www.google.fr")
.then(function (response) {
    // On récupère le resultat de la requête dans la varible "response"
    console.log(response)
})

La fonction "getHTTP" va retourner une Promise qui elle même va retourner le résultat de la requête HTTP, il nous suffit de chaîner la fonction avec ".then()" pour récupérer le résultat. Cette fonction nous permet de réduire le nombre de lignes et de simplifier la façon de faire des requêtes asynchrones (ici dans un navigateur Web avec l’objet "XMLHttpRequest").

Vous l’aurez peut être remarqué, si la requête ne fonctionne pas, la Promise ne se terminera jamais car nous ne prenons pas en charge les erreurs.

Pour gérer les erreurs, la façon la plus simple est d’utiliser la méthode "catch" de l’objet Promise. Comme "then", elle prend en paramètre un callback qui sera retourné par la Promise, à la différence près qu’elle prendra uniquement le callback que va rejeter la Promise (si il y a une erreur).

Voici le code précédent, adapté pour prendre en compte les erreurs :

function getHTTP(url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.onload = function() {
            if (this.status >= 200 && this.status < 300) {
                resolve(xhr.response);
            }
            else {
                reject({
                    status: this.status,
                    statusText: this.statusText
                });
            }
        };
        xhr.onerror = function () {
            reject({
                status: this.status,
                statusText: this.statusText
            });
        };
        xhr.open("GET", url);
        xhr.send();
    });
}

getHTTP("https://www.google.fr")
.then(function (response) {
    // On récupère le resultat de la requête dans la varible "response"
    console.log(response)
})
.catch(function (error) {
    // On affiche le code de retour de la requête
    console.log(error.status)
    // Puis le texte du status
    console.log(error.statusText)
})

Un problème qui arrive assez souvent en JavaScript, est le fait d’écrire des fonctions imbriquées les unes dans les autres, plus communément appelé le "callback hell".

Exemple :

getHTTP("https://www.google.fr")
.then(function (googleResponse) {
    getHTTP("https://www.supinfo.com")
    .then(function (supResponse) {
        getHTTP("https://example.com")
        .then(function (exampleResponse) {
            console("End of callBack hell !")
        })
        .catch(function (thirdError) {
            console.log(thirdError.status)
        })    })
    .catch(function (secondError) {
        console.log(secondError.status)
    })})
.catch(function (firstError) {
    console.log(firstError.status)
})

Ce bout de code fait 3 requêtes les unes après les autres en prenant en compte les erreurs pour chaque requête. Comme vous pouvez le remarquer ce n’est pas très lisible (voir pas du tout), alors pour palier à ce problème deux solutions s’offrent à nous :

  • exécuter toutes les requêtes en même temps

  • chaîner les Promises

Pour la première solution nous avons besoin de la méthode "Promise.all()", qui prend en paramètre un tableau de Promise et qui retournera une unique Promise, une fois que la liste de Promises sera traitée :

Promise.all([getHTTP("https://www.google.fr"), getHTTP("https://www.supinfo.com"), getHTTP("https://example.com")])
.then(function (data) {
    var googleResponse = data[0];
    var supResponse = data[1];
    var exampleResponse = data[2];
})
.catch(function (error) {
    console.log(error.status)
})

Cette solution est la plus courte (en terme de ligne) mais elle ne peut pas être utilisée dans le cas où l’on veut faire les appels un à un pour utiliser la valeur de la précédente requête avant de faire la suivante. Cela tombe bien, la seconde solution est là pour ça :

getHTTP("https://www.google.fr")
.then(function (googleResponse) {
    console.log(googleResponse);
    return getHTTP("https://www.supinfo.com")
})
.then(function (supResponse) {
    console.log(supResponse);
    return getHTTP("https://example.com")
})
.then(function (exampleResponse) {
    console.log("end");
})
.catch(function (error) {
    console.log(error.status)
})

Ici les Promise sont chaînées. Lors du retour de la première Promise nous pouvons alors faire un traitement spécifique sur le résultat, on retourne une autre Promise puis une fois celle-ci résolue on passe à la méthode "then" suivante et ainsi de suite.

Dans les deux exemples précédents, la méthode "catch" n’était présente qu’une seule fois, elle permet d’intercepter toutes les erreurs des précédentes Promise et également de garantir une syntaxe simple et plus compréhensible (on économise également quelques lignes de code).

Il est également possible d’ajouter un ".then()" supplémentaire à la fin du script qui permet d’exécuter une fonction dans tous les cas, peu importe le résultat des Promise précédentes :

getHTTP("https://www.google.fr")
.then(function (googleResponse) {
    return getHTTP("pageInexistan.te")
})
.then(function (res) {
    console.log(res);
})
.catch(function (error) {
    console.log(error.status)
})
.then(function () {
    console.log("Opérations terminées");
})

Async / Await

"Async" et "Await" sont deux opérateurs issus de l’EcmaScript 7 (2016), s’inspirant d’autres langages comme le Python ou le C#, qui ont beaucoup réjoui les développeurs JavaScript à leur arrivé. Avant que ces opérateurs soient standardisés, nous étions obligé d’utiliser de multitudes de callbacks qui rendaient souvent le code peu lisible. Maintenant, nous pouvons faire des appels à des fonctions asynchrone en utilisant la syntaxe d’un code synchrone, ce qui rend visuellement plus logique et plus simple à comprendre.

Voici un exemple pour illustrer cet avantage :

async function main() {
    var googleResponse = await getHTTP("https://www.google.fr")
    console.log(googleResponse);
    var supResponse = await getHTTP("https://www.supinfo.com")
    console.log(supResponse);
}

main()

Comme on peut le voir, il n’y a aucun callback, et le code se lit très facilement.

Le fonctionnement technique de ces opérateurs est assez simple :

  • On place "async" en premier lieu lorsque l’on déclare une fonction pour préciser que celle-ci est asynchrone.

  • On place "await" juste avant d’appeler une fonction asynchrone, ce qui aura pour but d’attendre la réponse de cette dernière.

Tout ça n’est pas magique. En réalité, Async va automatiquement transformer la fonction en une Promise et va résoudre celle-ci avec le résultat retourner par "return". Grâce à Async, on a accès à Await dans la fonction, qui lui permet de forcer le reste du code à attendre que la Promise soit résolue et qu’elle retourne un résultat (ou une erreur).

Await fonctionne seulement dans une fonction précédée de Async et permet d’attendre uniquement une Promise.

Exemple avec un callback :

async function sleep() {
    await setTimeout(function () {
        return "wake up !"
    }, 1000);
}

sleep().then(function(res) {
    console.log(res);
})

Vous pouvez tester par vous-même, le code ci-dessus n’affichera rien à l’écran. Comme Await n’attend pas les callbacks, la fonction "sleep" va se terminer directement sans rien retourner.

Pour obtenir une fonction "sleep" comme celle présente en Bash, nous pourrions faire comme cela :

function sleep(time){
    return new Promise(function(resolve) {
        setTimeout(function () {
            resolve()
        }, time);
    })
}
async function main() {
    console.log("Sleeping..");
    await sleep(1000)
    return "wake up !"
}

main().then(function(res) {
    console.log(res);
})

Async et Await ne remplacent pas les Promises, ils permettent uniquement de simplifier l’appel de celles-ci.

La bonne pratique lorsque l’on utilise ces opérateurs, est de découper toutes les tâches asynchrones en différentes fonctions qui retournent une Promise. Ensuite, il ne reste plus qu’à appeler ces fonctions avec Await dans la partie principale du code, et adieu les callbacks !

Pour prendre en charge les erreurs des fonctions utilisant Async/Await, on utilise simplement le bon vieux "try/catch" :

function errorFunction(){
    return new Promise(function(resolve, reject) {
        setTimeout(function () {
            reject("There is a bug !")
        }, 1000);
    })
}

async function main() {
    try {
        await errorFunction()
    } catch (error) {
        console.log(error);
    }
}

main()

Dans le cas si dessus, nous pourrions également prendre en compte les erreurs lorsque l’on appelle la fonction "main" en chaînant avec la méthode "catch" :

async function main() {
    await errorFunction()
}

main()
.then(function () {
    console.log("done");
})
.catch(function (error) {
    console.log(error)
})

Nous avons vu précédemment dans l’article qu’il est possible d’exécuter une liste de Promises et d’attendre le retour de celles-ci avant de continuer le déroulement du script, alors comment adapter ce procédé avec les opérateurs Async/Await ?

Voilà un exemple :

async function main() {
    var [googleResponse, supResponse] = await Promise.all([getHTTP("https://www.google.fr"), getHTTP("https://www.supinfo.com")])
    console.log(googleResponse);
    console.log(supResponse);
}

main()

On précède simplement la méthode "Promise.all()" (qui retournera un tableau de résultats) avec "await".

Compatibilité

Le JavaScript est principalement utilisé dans la programmation Web, coté client, il est donc exécuté sur nos navigateurs. Malheureusement, les navigateurs ne sont pas tous encore compatibles avec l’objet Promise et les opérateurs Async / Await, comme peuvent nous le montrer ces tableaux :

Liste de compatibilité des principaux navigateurs pour les Promises:

Liste de compatibilité des principaux navigateurs pour les opérateurs Async/Await :

Parmi les principaux navigateurs Web, seul Internet Explorer n’est pas compatible, mais il reste un des navigateurs les plus utilisé et on ne peut pas négliger cela, il va donc falloir attendre avant d’utiliser les Promises sur votre site Web. Fort heureusement, il existe des librairies pour palier à ce problème, comme le très célèbre JQuery.

JavaScript est de plus en plus utilisé du coté serveur avec NodeJS, qui supporte très bien les Promises dans les dernières versions stables, c’est donc une plate forme adéquate pour vous exercer avec les fonctions asynchrones.

Conclusion

Nous avons vu dans cet article la notion de programmation asynchrone en JavaScript ainsi que les outils qui permettent de développer tout en utilisant les bonnes pratiques.

Il est important de se rappeler que les opérateurs Async/Await utilisent les Promises, ne les remplacent pas et permettent simplement de faciliter l’écriture et la compréhension du code.

Avant de commencer le développement d’un nouveau site Web qui utilise les Promises, il est conseillé de vérifier si la version des navigateurs ciblés est compatible (il en est de même pour NodeJS).

Ressources

Quelques liens utiles pour approfondir vos connaissances :

A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Toeic, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels