Javascript asynchrone, les Promises à la rescousse

Tout est dans le titre. L’ES6 a apporté beaucoup de nouveautés, entre hype et réelle utilité, beaucoup d’encre a coulée. Les Promises ont été à l’honneur car elles sont un ajout notable au langage et pemettraient, apparemment, de nous affranchir du callback hell. Voyons dans cet article comment les utiliser pour une meilleure gestion du flux.

Dans un langage asynchrone tel que le JS, de nombreuses fonctions sont asynchrones et nécessitent une structure particulière puisque la suite du code est exécutée sans attendre. Traditionellement en JavaScript, on utilise les callbacks pour cela.

Sans entrer dans le débat du pour ou contre callback, voyons quand même un petit exemple :

appelReseau(url, (error, html) => {
    if (error) {
        console.error(error);
        return;
    }

    console.log(html);
});

Jusque là, pas de problème, ça semble visuellement clair et si le code est bien structuré [en], il n’y a pas trop de raison de tomber dans l’enfer des callbacks imbriqués.

Afin de comparer, et si vous avez besoin d’une petite intro, ou d’un rafraîchissement, à propos des promesses, je vous invite à lire l’article de Putain de Code. Il est clair et concis !

Au delà des promesses classiques, il arrive que l’on doive exécuter une fonction prenant pour paramètres les résultats de plusieurs autres fonctions. Par exemple, chez Buzeo, pour des opérarions de fusion ou supression de compte, nous devons vérifier si le compte en question possède des photos, des vidéos ou des musiques.

Cela nécessite trois appels à la base de données. On pourrait attendre que le premier appel soit terminé, lancer le second dans le callback de la première fonction etc… Mais ce serait dommage car on ne tire absolument pas parti de la nature asynchrone de JavaScript.

L’autre solution consiste donc a tenir le compte des fonctions qui ont terminé et d’exécuter la suite lorsque toutes les fonctions ont retourné leur résultat.

// on utilise une closure pour conserver le compte des fonctions
const mediaReady = (() => {
    let completeStatus = 0;
    let mediaCount = 0;

    return function (mediaNumber) {
        mediaCount += mediaNumber;

        if (++completeStatus === 3) {
            // ici on peut passer à la suite
            nextCall(mediaCount);
        }
    };
})();
    
Video.count({ userId }, (err, videoNumber) {
    if (error) {
        console.error(error);
        return;
    }

    mediaReady(videoNumber);
});

Audio.count({ userId }, (err, audioNumber) {
    if (error) {
        console.error(error);
        return;
    }

    mediaReady(audioNumber);
});

Image.count({ userId }, (err, imageNumber) {
    if (error) {
        console.error(error);
        return;
    }

    mediaReady(imageNumber);
});

Nous avons ici la closure qui se charge de conserver les résultats des différentes fonctions et d’effectuer l’appel suivant lorsqu’on est en possession de tous les résultats.

Ce code est difficilement maintenable dans la mesure où il faudra modifier (++completeStatus === 3) dès lors que l’on ajoute ou supprime des fonctions. Les Promises représentent une vrai solution à ce problème grâce à Promise.all.

Cette magnifique méthode retourne une Promise qui résout lorsque toutes les Promises de l’itérable passé en argument sont résolues. En utilisant Promise.all, l’exemple précédent devient beaucoup plus concis et maintenable.

Nous utilisons Mongoose dans l’exemple. Le seul prérequis pour l’utilisation des Promises ES6 est d’avoir Mongoose >= 4. Par ailleurs, dans la mesure où ce dernier peut être utilisé avec plusieurs types de Promises, on lui spécifie juste d’utiliser les Promises ES6 natives :

// on précise à mongoose d'utiliser les promises ES6
// le réglage est global et n'empêche pas d'utiliser également
// les callbacks
mongoose.Promise = global.Promise;

let mediaCount = 0;

const videoPromise = Video.count({ userId });
const audioPromise = Audio.count({ userId });
const imagePromise = Image.count({ userId });

Promise.all([videoPromise, audioPromise, imagePromise])
.then((result) => {
    // nos résultats sont dispo ici 
    // lorsque les Promises ont toutes résolues
    result.forEach((el) => {
        mediaCount += el;
    });

    nextCall(mediaCount);
})
.catch((err) => {
    console.error(err);
});

Tout le monde devrait s’accorder sur le fait que c’est plus court, plus simple et plus maintenable. Les Promises ne sont pas à mon goût la solution à tous les problèmes, comme certains se plaisent à les présenter. Cependant, cette méthode dont on parle trop peu, permet d’exploiter toute la puissance asynchrone du JavaScript en conservant un code lisible.

Il n'y a pas encore de commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *