JavaScript sans callback

Des méthodes alternatives et simple permettent de s'affranchir de 'L'enfer des callback' propre à ce langage.

1) Quand on utilise un callback

Nous définissous une fonction simple en example, mais qui en production est généralement complexe et asynchrone.

function fibo(n) {
    if (n < 2) return n
    return fibo(n-2) + fibo(n-1)
}

Lorsque nous appelons cette fonction qui est supposée être asynchrone, le traitement continue sans attendre le résultat. Mais si nous voulons afficher le résultat, ce qui arriver à la fin du traitement, comment faire?

Voici la solution, mettre la fonction d'affichage en paramètre:

function getFibo(fn) {
  var x = fn(20)
  fn(x)	
}

Et on appelle la fonction avec un callback:

getFibo(function(x) {
  console.log("Fibo(20) = " + x) 
})

C'est ce callback que l'on veut supprimer.

2) Une fonction nommée pour éviter le code spaghetti

La définition de fonction donnée en paramètre de getFibo est déplacée dans une fonction nommé, et remplacé par une référence au nom de cette fonction.

function display(x) {
  console.log("Fibo(20) = " + x) 
}

getFibo(display)

La fonction est ainsi réutilisable, et ce code source l'est d'autant plus qu'il est plus clair et plus lisible.

On parle aussi des générateurs pour remplacer les callbacks...

3) Générateurs?

Si au lieu d'utiliser une fonction nommée on utilisait un générateur, cela donnerait le code suivant:

function *gen() {
  var fiboResult	
  yield fiboResult = fibo(20)
  yield console.log("Generator: fibo(20)=" + fiboResult)
}

var g = gen();
g.next()  // appel de  fibo
g.next()  // affichage

Cela produit aussi un code clair et lisible mais le problème est que yield n'est pas asynchrone, cela ne fonctionnerait pas si fibo était une fonction asynchrone. Les générateurs ne remplacent les callbacks que si on les associe à des frameworks de coroutines tels que Bluebird, Co, Q.

Une solution plus appropriée, sans avoir à charger de module externe, les promises...

4) Promise au lieu de callbacks

Les promises fonctionnent de façon asynchrone et conviennent parfaitement pour remplacer les callbacks. Le code équivalent au code en 1) mais sans le callback s'écrit ainsi:

function display2(x) {
  console.log("Promise: fibo(20)="  + x)
}

function p(fn) {
  var fiboResult =  fibo(20)
  fn(fiboResult)
}

var promise = new Promise(p)
promise.then(display2)

Si vous n'êtes pas familier avec ce concept apparu avec ECMAScript 6, vous pouvez consulter ce tutoriel: Promise en JavaScript.

La promise est construite sur la fonction p qui appelle fibo, et a la fonction fn en paramètre. Celle-ci référence la fonction display2 qui affiche le résultat final par la méthode then.

Il y a toujours une fonction en paramètre, mais l'avantage des promises devient réellement évident pour éviter plusieurs callbacks imbriqués. On peut en donner un exemple...

5) Callbacks imbriqués

Les choses se compliquent un peu quand on enchaîne plusieurs processus:

  1. On calcule la suite de Fibonacci pour le nombre 20.
  2. Puis on multiplie le résultat par deux.
  3. Et après on affiche le résultat final.

On suppose que ces opérations sont asynchrones, même si on a simplifié la démonstration avec un code qui ne l'est pas.

function getDouble(dbl) {
  var x = fibo(20)
  dbl(x)		
}

function dblFibo(x, fn) {
  var d = x * 2
  fn(d)
}

getDouble(function(x) {
  dblFibo(x, function(y) {
    console.log("Double callback, fibo(20) * 2 = " + y)
  }) 
})	

La fonction dblFibo est définie en callback de la fonction getDouble qui calcule le premier résultat.
L'affichage du résultat final est défini en callback de la fonction dblFibo qui multiple le résultat pas deux

Le fait est que cela devient un peu plus compliqué à lire (et le serait encore plus avec le code d'une application réelle), et on va voir comment simplifier tout cela avec des promises.

6) Chaîner les promises

Le code suivant remplace l'ensemble du code précédent:

function p1(fn) {
  var fiboResult = fibo(20) 
  fn(fiboResult)
}

function dblFibo2(x) {
  return x * 2
}

function display3(x) {
  console.log("Chaining promise: fibo(20) * 2 = "  + x)
}

var promise1 = new Promise(p1)

promise1.then(dblFibo2).then(display3)

La promise est construite sur la fonction p1 qui à en paramètre une seconde fonction dblFibo, qui multiplie le résultat par deux. Cette seconde fonction est invoquée par le premier then.

Le second then invoque la fonction d'affichage display3 après dblFibo, même si display3 n'est pas définie en callback: c'est l'avantage des then qui se succèdent toujours dans le temps, même sur des traitements asynchrones.

Tous ces exemples sont réunis dans un unique fichier JavaScript que vous pouvez exécuter en ligne de commande avec io.js.

Télécharger la démonstration.

Vous noterez en exécutant le script que le résultat des promises s'affiche après tous les autres, et non dans l'ordre où elle apparaissent dans le fichier JS. C'est normal, parce qu'elle sont réellement asynchrones et pas les autres exemples.

© 20 juillet 2015 Xul.fr