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)
}

On suppose que cette fonction est asynchrone, donc si on l'appelle, le traitement continue sans attendre le résultat. Si on fait:

console.log(fibo(20))

cette commande est exécutée avant que le résultat ne soit calculé. Comment faire pour l'exécuter à la fin du calcul?

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

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

Et on appelle cette fonction avec un callback:

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

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

On parle quelquefois de générateurs pour remplacer les callbacks...

2) Générateurs?

Si au lieu d'utiliser un callback 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 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...

3) 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...

4) 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.

5) Chaîner les promises

Le code suivant obtient le même résultat en étant plus lisible...

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.

6) async/await

Implémenté depuis peu dans les navigateurs et dans Node.js.

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

function pFibo(n) {
    var p = new Promise(
        function(resolve) {
            setTimeout(function() { resolve(fibo(n)) }, 1000)    
        }
    )
    return p
}

async function getFibo(n) {
   var f = await pFibo(n)
   console.log("Fibo=" + f)
}

console.log("Waiting...")
getFibo(20)

setTimeout est ici pour la démonstration seulement, tout comme le calcul de Fibonacci. Dans la pratique on attend le résultat d'un processus asynchrone.
La fonction qui contient await doit être déclarée async, et la fonction dont on attend le résultat doit contenir une promise. C'est donc un peu plus compliqué à ce niveau, mais l'utilisation de cette fonction est beaucoup plus simple.

Si vous appelez pFibo sans await, vous obtiendrez Fibo=Object[Promise] et non le résultat parce que la ligne qui suit l'appel sera exécutée avant la fin de l'exécution de pFibo.


Tous ces exemples sont inclus dans un archive zip à télécharger, et peuvent être exécutés en ligne de commande avec node.js.

Télécharger les démonstrations.

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 - Mise à jour en 2017 Xul.fr