Skip to content

Programmation Asynchrone JavaScript et Callbacks

L'Asynchronisme dans les Langages de Programmation

Les ordinateurs sont asynchrones par conception.

Asynchrone signifie que les choses peuvent se produire indépendamment du flux principal du programme.

Dans les ordinateurs grand public actuels, chaque programme s'exécute pendant un créneau horaire spécifique, puis interrompt son exécution pour laisser un autre programme poursuivre son exécution. Ce processus se répète si rapidement qu'il est impossible de le remarquer. Nous pensons que nos ordinateurs exécutent de nombreux programmes simultanément, mais c'est une illusion (sauf sur les machines multiprocesseurs).

Les programmes utilisent en interne des interruptions, un signal émis au processeur pour attirer l'attention du système.

Ne rentrons pas dans les détails internes pour le moment, mais gardez simplement à l'esprit qu'il est normal que les programmes soient asynchrones et interrompent leur exécution jusqu'à ce qu'ils aient besoin d'attention, permettant à l'ordinateur d'exécuter d'autres choses entre-temps. Lorsqu'un programme attend une réponse du réseau, il ne peut pas bloquer le processeur jusqu'à la fin de la requête.

Normalement, les langages de programmation sont synchrones et certains offrent un moyen de gérer l'asynchronisme dans le langage ou via des bibliothèques. C, Java, C#, PHP, Go, Ruby, Swift et Python sont tous synchrones par défaut. Certains d'entre eux gèrent les opérations asynchrones en utilisant des threads, créant un nouveau processus.

JavaScript

JavaScript est synchrone par défaut et monothreadé. Cela signifie que le code ne peut pas créer de nouveaux threads et s'exécuter en parallèle.

Les lignes de code sont exécutées en série, l'une après l'autre, par exemple :

js
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();

Mais JavaScript est né dans le navigateur, son travail principal, au début, était de répondre aux actions de l'utilisateur, comme onClick, onMouseOver, onChange, onSubmit, etc. Comment pouvait-il le faire avec un modèle de programmation synchrone ?

La réponse se trouvait dans son environnement. Le navigateur fournit un moyen de le faire en fournissant un ensemble d'API capables de gérer ce type de fonctionnalité.

Plus récemment, Node.js a introduit un environnement d'E/S non bloquant pour étendre ce concept à l'accès aux fichiers, aux appels réseau, etc.

Callbacks

On ne peut pas savoir quand un utilisateur va cliquer sur un bouton. On définit donc un gestionnaire d'événements pour l'événement de clic. Ce gestionnaire d'événements accepte une fonction, qui sera appelée lorsque l'événement est déclenché :

js
document.getElementById('button').addEventListener('click', () => {
  // élément cliqué
});

C'est ce qu'on appelle un callback.

Un callback est une simple fonction passée en valeur à une autre fonction, et qui ne sera exécutée que lorsque l'événement se produit. On peut faire cela car JavaScript possède des fonctions de première classe, qui peuvent être assignées à des variables et passées à d'autres fonctions (appelées fonctions d'ordre supérieur).

Il est courant d'encapsuler tout votre code client dans un écouteur d'événements load sur l'objet window, qui exécute la fonction callback uniquement lorsque la page est prête :

js
window.addEventListener('load', () => {
  // fenêtre chargée
  // faites ce que vous voulez
});

Les callbacks sont utilisés partout, pas seulement dans les événements DOM.

Un exemple courant est l'utilisation de temporisateurs :

js
setTimeout(() => {
  // s'exécute après 2 secondes
}, 2000);

Les requêtes XHR acceptent également un callback, dans cet exemple en assignant une fonction à une propriété qui sera appelée lorsqu'un événement particulier se produit (dans ce cas, l'état de la requête change) :

js
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4) {
    xhr.status === 200 ? console.log(xhr.responseText) : console.error('error');
  }
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();

Gestion des erreurs dans les callbacks

Comment gérer les erreurs avec les callbacks ? Une stratégie très courante est d'utiliser ce que Node.js a adopté : le premier paramètre de toute fonction callback est l'objet erreur : les callbacks erreur-en-premier.

S'il n'y a pas d'erreur, l'objet est nul. S'il y a une erreur, il contient une description de l'erreur et d'autres informations.

js
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
  if (err) {
    // gérer l'erreur
    console.log(err);
    return;
  }
  // pas d'erreurs, traiter les données
  console.log(data);
});

Le problème des callbacks

Les callbacks sont géniaux pour les cas simples !

Cependant, chaque callback ajoute un niveau d'imbrication, et lorsque vous avez beaucoup de callbacks, le code devient très rapidement compliqué :

js
window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {
        // votre code ici
      });
    }, 2000);
  });
});

Ce n'est qu'un code simple à 4 niveaux, mais j'ai vu beaucoup plus de niveaux d'imbrication et ce n'est pas amusant.

Comment résoudre ce problème ?

Alternatives aux callbacks

À partir d'ES6, JavaScript a introduit plusieurs fonctionnalités qui nous aident avec le code asynchrone sans utiliser de callbacks : les Promises (ES6) et Async/Await (ES2017).