Nous vous avons déjà parlé des Promises et de leur utilité dans un précédent article (Asynchronicité : les promesses de Javascript).
Aujourd’hui nous allons vous montrer un nouvel exemple d’utilisation des Promises en conjonction avec une fonction introduite par ES5 : Array.prototype.reduce().

Array.prototype.reduce()

reduce() est une fonction qui s’applique sur un tableau, peut retourner n’importe quel type d’objet, et prend en paramètre un callback (qui sera appelé pour chaque élement du tableau, de la gauche vers la droite), ainsi qu’une valeur par défaut.

var value = [1,2,3].reduce(fn, defaultValue)

La fonction en paramètre prend plusieurs arguments :

function fn(previousValue, currentValue, currentIndex, array){
//Traitement
};

  • previousValue contient la valeur renvoyée par la fonction pour le précédent élément du tableau, ou si on est sur le premier élément, defaultValue .
  • currentValue correspond à l’élément du tableau en cours d’évaluation
  • currentIndex est l’index dans le tableau de l’élément en cours d’évaluation
  • et array est une référence vers le tableau sur lequel on a appelé reduce

reduce peut ainsi être utilisé facilement pour calculer la somme des éléments d’un tableau par exemple.

var primeNumbers = [1,3,5];
var sum = primeNumbers.reduce(function(previousValue, currentValue, currentIndex, array){
console.log(Index ${currentIndex} - currentValue : ${currentValue} - previousValue : ${previousValue})
return previousValue + currentValue;
}, 0);
console.log("Somme :", sum);

Ainsi le code ci-dessus affichera :

Index 0 - currentValue : 1 - previousValue : 0
Index 1 - currentValue : 3 - previousValue : 1
Index 2 - currentValue : 5 - previousValue : 4
Somme : 9

Si la fonction reduce est appelée sans valeur par défaut, alors son comportement est modifié :

  • Le callback n'est alors appelé qu'à partir du deuxième élément du tableau
  • previousValue contient la valeur renvoyée par la fonction pour le précédent élément du tableau, ou si on est sur la première itération, le premier élément du tableau

Ainsi, si on reprend notre code précédent en omettant la valeur par défaut :

var sum = primeNumbers.reduce(function(previousValue, currentValue, currentIndex, array){
console.log(
Index ${currentIndex} - currentValue : ${currentValue} - previousValue : ${previousValue}`)
return previousValue + currentValue;
});
console.log("Somme :",sum);

Cela affichera :

Index 1 - currentValue : 3 - previousValue : 1
Index 2 - currentValue : 5 - previousValue : 4
Somme : 9

Vous pouvez voir que le traitement commence directement au currentIndex 1, et se sert du premier élément du tableau en tant que previousValue.

Vous pourrez trouver plus d’information sur reduce sur MDN : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Array/reduce

Pouvoir calculer des sommes c’est bien, mais cela n’est pas ce qu’il y a de plus palpitant je vous l’accorde. Voyons comment rendre cela plus intéressant.

Le problème

Nous avons eu récemment une problématique de performance sur un de nos projets. De par une modification dans l’interface utilisateur, il était possible d’effectuer 100 appels AJAX simultanés vers un seul et même service. Comme le traitement de ce service était assez conséquent, le serveur était rapidement surchargé, ce qui entraînait une réelle baisse des performances sur l’ensemble de l’application.

See the Pen Appels simultanés – Congestion by Nicolas Chevobbe (@nchevobbe) on CodePen.12994

Dans l’exemple ci-dessus, nous avons créé une fonction qui simule la surcharge d’un serveur en appliquant un setTimeout exponentiel suivant le nombre d’appels en cours sur le serveur. On peut voir que certains appels prennent énormément de temps, et dans notre cas, on pouvait avoir des timeouts, voire un freeze complet du serveur.

Afin de régler ce problème, nous avons mis en place une première solution qui consistait à n’effectuer qu’un seul appel à la fois. Et c’est là que reduce entre en jeu. L’astuce consiste à retourner dans le callback de reduce une Promise, que l’on pourra alors utiliser à l’itération suivante de la boucle.

callers.reduce(function(previousValue, currentValue, currentIndex, array){
return new Promise(function(resolve, reject){
//Traitement
});
});

Ainsi, previousValue sera une Promise, sur laquelle on pourra s’appuyer pour effectuer le traitement de l’élément courant :

callers.reduce(function(previousValue, currentValue, currentIndex, array){
return new Promise(function(resolve, reject){
//Une fois la promise précédente complétée
previousValue.then(function(){
//Traitement
});
});
});

Ci-dessus, on se sert de la Promise retournée par l’itération précédente pour n’effectuer le traitement de l’objet courant que lorsque le traitement précédent est terminé. Dans notre cas, on peut appeler la fonction qui effectue la requête AJAX vers le service ( et qui renvoie une Promise ).

callers.reduce(function(previousValue, currentValue, currentIndex, array){
return new Promise(function(resolve, reject){
//Une fois la promise précédente complétée
previousValue.then(function(){
// Alors on peut appeler le service
// Et lorsque l'appel est complété, résoudre la promise
callService(currentValue).then(resolve)
});
});
});

Mais en l’état, il nous manque encore quelque chose pour que cela fonctionne. Vu que l’on appelle previousValue.then , il faut que celui-ci soit tout le temps une Promise. Il nous faut donc gérer la première boucle dans le callback. Ce que l’on va pouvoir faire en se servant de la valeur par défaut que l’on peut passer dans reduce. L’idée c’est de passer en paramètre une Promise qui se résout immédiatement afin de pouvoir l’utiliser lors de la première itération du callback. C’est précisément ce que Promise.resolve() nous permet d’avoir : une Promise déjà résolue.

callers.reduce(function(previousValue, currentValue, currentIndex, array){
return new Promise(function(resolve, reject){
//Une fois la promise précédente complétée
previousValue.then(function(){
// Alors on peut appeler le service
// Et lorsque l'appel est complété, résoudre la promise
callService(currentValue).then(resolve)
});
});
},
/* La Promise se résout immédiatement */
Promise.resolve());

Avec ce mécanisme, on pourra donc simplement effectuer l’ensemble de nos appels les un à la suite des autres, comme on le ferait avec une requête AJAX synchrone sans en avoir les inconvénients ( i.e. un freeze complet de l’interface car la synchronicité de la requête bloque le thread Javascript ).

See the Pen Appels séquentiels by Nicolas Chevobbe (@nchevobbe) on CodePen.12994

Ce qui est encore mieux, c’est que cela nous donne aussi un moyen de savoir quand l’ensemble des traitements est terminé. En effet, reduce retourne une valeur : celle qui est renvoyée par la dernière itération dans le callback. Dans notre cas, ce sera donc la dernière Promise, qui sera résolue lorsque le dernier appel aura été effectué. Dans notre exemple, on se sert de cette Promise pour arrêter le chronomètre.

// On démarre le chronomètre
startTimer();

var appelsPromise = callers.reduce(function(previousValue, currentValue, currentIndex, array){
return new Promise(function(resolve, reject){
//Une fois la promise précedente complétée
previousValue.then(function(){
// Alors on peut appeler le service
// Et lorsque l'appel est complété, résoudre la promise
callService(currentValue).then(resolve)
});
});
}, Promise.resolve());

// Lorsque l'ensemble des appels on été effectués
appelsPromise.then(function(){
// Alors on arrête le chronomètre
stopTimer();
});

Comme vous le voyez sur l’exemple, nous n’avons plus de problème de congestion du serveur. On perd cependant un avantage car on effectue un seul traitement à la fois, alors que le serveur pourrait en traiter une dizaine en simultané sans baisse significative de performance. On peut alors améliorer notre solution en effectuant des appels par groupe de 10, tout en gardant le même système : n’appeler qu’un groupe à la fois, les uns à la suite des autres.

// On démarre le chronomètre
startTimer();

// On construit un tableau avec des groupes de 10 appels
var batches = [];
while(callers.length > 0){
batches.push(callers.splice(0,10));
}

// Et on appelle reduce sur ce tableau
var appelsBatchPromise = batches.reduce(function(previousValue, currentValue, currentIndex, array){
// Tout comme pour l'exemple précedent, on retourne une promise
return new Promise(function(resolve, reject){
//Une fois que la promise du groupe précedent est complétée
previousValue.then(function(){
// Alors on peut appeler le service pour l'ensemble des appels du groupe courant
// On construit un tableau de Promise des appels
var callsPromise = currentValue.map(function(call){
return callService(call);
});

//Et lorsqu'ils sont tous terminés
Promise.all(callsPromise)
//Alors on résout la promise pour le groupe courant
.then(resolve);
});
});
}, Promise.resolve());

// Lorsque l'ensemble des groupes ont été traités
appelsBatchPromise.then(function(){
// Alors on arrête le chronomètre
stopTimer();
});

See the Pen Appels batchs by Nicolas Chevobbe (@nchevobbe) on CodePen.12994

Le gain de temps est ici significatif par rapport à l’appel séquentiel, tout en évitant de surcharger le serveur : tout le monde gagne !

Conclusion

Ceci n’est qu’un exemple de ce qui peut être fait avec l’association des Promises et de la fonction reduce. Le champ d’application de ce pattern est bien plus vaste pour peu que vous ayez à gérer des séquences de traitements asynchrones.

Pour aller plus loin

Asynquence ( ou ASQ ) est une librairie Javascript dont le but est de gérer plus facilement des séquences de traitements asynchrones.
Tasks.js est aussi une librairie Javascript, qui s’appuie sur les fonctions generator pour permettre de gérer des séquences asynchrones tout en gardant un code qui parait synchrone ( à l’instar de ce que permettra async/await ).