Publié le

Les bases de la programmation asynchrone en JavaScript/Node.js

Auteurs
Illustration graphique duu fonctionnement d'un runtime de javascript avec l'event loop

JavaScript s'exécute en mono-thread, traitant notre code de manière synchronisée, c'est-à-dire une instruction après l'autre. Conçu à l'origine pour les navigateurs web, son rôle était de rendre les pages web dynamiques et interactives. Mais alors, comment gérer des événements déclenchés par les utilisateurs sans bloquer le flux principal ? La réponse réside dans les mécanismes asynchrones tels que les "callbacks".

Dans cet article, je vais vous montrer les principaux concepts de la programmation asynchrone en JavaScript, en particulier sous Node.js. Vous allez découvrir le cœur de l'asynchronisme, l'event loop, et la manière dont notre code est traité. Une fois ces notions maîtrisées, vous serez armés pour écrire des programmes JavaScript optimisés.

Ne manquez pas mes prochains articles

Prérequis

Comme à mon habitude, je vous rappelle qu'il est nécessaire d'avoir Node.js installé. Une connaissance de base en JavaScript est aussi essentielle. Pour la rédaction, je vous suggère Visual Studio Code.

L'environnement d'exécution : Runtime

Un "runtime" JavaScript est un contexte permettant l'exécution de code JavaScript, comme Node.js ou ceux intégrés dans les navigateurs. Il comprend plusieurs composants clés:

  • La Heap : C'est le lieu où sont stockées les variables et les déclarations de fonctions.
  • La pile d'appels (Call Stack) : Chaque fonction appelée est ajoutée à la pile et exécutée. Si cette fonction en appelle une autre, son exécution est mise en pause, et la nouvelle fonction est traitée. Cette pile fonctionne selon un système LIFO (dernier entré, premier sorti).
  • La file d'attente de rappel (Callback Queue) : Elle stocke les fonctions de rappel en attendant leur exécution, selon un système FIFO (premier entré, premier sorti).
  • L'Event Loop : Sa mission est de transférer les fonctions de la queue vers la pile d'appels une fois cette dernière vide.

Pour mieux illustrer ces concepts, prenons un exemple :

function test() {
  return console.log('Je suis un test')
}

function executeLeTest() {
  return test()
}

executeLeTest()

Durant l'exécution, l'interpréteur lit le fichier et stocke les fonctions en mémoire. Lorsqu'il rencontre l'appel à executeLeTest(), cette fonction est ajoutée à la pile. La fonction test() est ensuite appelée, ajoutée à la pile, et exécutée. Après l'exécution complète, chaque fonction est retirée de la pile, l'une après l'autre.

Les fonctions de rappel (Callbacks)

Un callback est une fonction passée en argument à une autre, qui sera invoquée une fois cette dernière terminée. Historiquement, les callbacks ont été le principal moyen de gérer des opérations asynchrones en JavaScript.

Prenons un exemple simple avec Node.js :

mkdir afficheBlagues
cd afficheBlagues
npm init -y
npm i request
touch index.mjs

Ajoutez ensuite le code suivant dans le fichier index.mjs:

import request from 'request'

function callback(error, response, body) {
  if (error) {
    console.error(`Erreur de l'API : ${error.message}`)
    return
  }

  if (response.statusCode !== 200) {
    console.error(`La requête a échoué.`)
    return
  }

  const joke = JSON.parse(body)
  console.log(joke)
}

request('https://api.chucknorris.io/jokes/random', callback)

En exécutant ce code, une blague de Chuck Norris s'affiche dans la console. Si vous observez attentivement, la méthode request prend un callback en argument. Ce callback est invoqué une fois la requête réseau terminée.

Supposons maintenant que vous souhaitiez écrire cette blague dans un fichier blague.json. Utilisons le module fs de Node pour cela :

import request from 'request'
import fs from 'fs'

function ecrireDansFichier(error, data) {
  if (error) {
    console.error("Erreur lors de l'écriture dans le fichier.")
    return
  }
  console.log('Blague écrite avec succès dans le fichier.')
}

function callback(error, response, body) {
  if (error) {
    console.error(`Erreur de l'API : ${error.message}`)
    return
  }

  if (response.statusCode !== 200) {
    console.error(`La requête a échoué.`)
    return
  }

  const joke = JSON.parse(body)
  fs.writeFile('blague.json', JSON.stringify(joke, null, 2), ecrireDansFichier)
}

request('https://api.chucknorris.io/jokes/random', callback)

Ce code fait sensiblement la même chose que précédemment, sauf qu'il écrit maintenant la blague dans un fichier. Mais comme vous pouvez le voir, avec l'ajout de nouvelles opérations asynchrones, le code devient rapidement plus compliqué. Ce problème est souvent appelé "callback hell".

Ne manquez pas mes prochains articles

Les Promesses

Les promesses sont une alternative aux callbacks et offrent une manière plus propre et lisible de gérer l'asynchronisme. Une promesse représente une valeur qui pourrait être disponible maintenant, dans le futur, ou jamais.

Pour illustrer l'utilisation des promesses, utilisons axios, une bibliothèque basée sur les promesses pour effectuer des requêtes HTTP :

npm i axios
import axios from 'axios'
import fs from 'fs/promises'

async function recupererBlague() {
  try {
    const response = await axios.get('https://api.chucknorris.io/jokes/random')
    await fs.writeFile('blague.json', JSON.stringify(response.data, null, 2))
    console.log('Blague écrite avec succès dans le fichier.')
  } catch (error) {
    console.error('Il y a eu une erreur lors de la récupération de la blague.')
  }
}

recupererBlague()

Grâce aux promesses et à async/await, le code est plus structuré et facile à suivre.

Conclusion

Le JavaScript asynchrone peut sembler compliqué au premier abord, mais avec une compréhension solide des mécanismes sous-jacents et les bons outils, vous pouvez écrire du code asynchrone propre et efficace. Que vous choisissiez d'utiliser des callbacks, des promesses, ou async/await, l'important est de comprendre les avantages et les inconvénients de chaque approche et de choisir celle qui convient le mieux à votre situation.

Ne manquez pas mes prochains articles