Fonctionnement de Node.js

Effectué

Cette unité explique comment Node.js gère les tâches entrantes dans le runtime JavaScript.

Types de tâches

Les applications JavaScript ont deux types de tâches :

  • Tâches synchrones : Ces tâches se produisent dans l’ordre. Elles ne dépendent pas d’une autre ressource pour être accomplies. Il peut s’agir par exemple d’opérations mathématiques ou de manipulation de chaînes.
  • Tâches asynchrones : Ces tâches peuvent ne pas être effectuées immédiatement, car elles dépendent d’autres ressources. Il peut s’agir par exemple de requêtes réseau ou d’opérations de système de fichiers.

Étant donné que vous souhaitez que votre programme s’exécute aussi rapidement que possible, vous voulez que le moteur JavaScript puisse continuer à fonctionner pendant qu’il attend une réponse d’une opération asynchrone. Pour ce faire, il ajoute la tâche asynchrone à une file d’attente de tâches et continue à travailler sur la tâche suivante.

Gérer la file d’attente de tâches avec une boucle d’événements

Node.js utilise l’architecture basée sur les événements du moteur JavaScript pour traiter les requêtes asynchrones. Le diagramme suivant illustre le fonctionnement d’une boucle d’événements V8, à un niveau élevé :

Diagram showing how Node J S uses an event-driven architecture where an event loop processes events and returns callbacks.

Une tâche asynchrone, indiquée par la syntaxe appropriée (illustrée ci-dessous), est ajoutée à la boucle d’événements. La tâche inclut le travail à effectuer et une fonction de rappel pour recevoir les résultats. Une fois l’opération intensive terminée, la fonction de rappel est déclenchée avec les résultats.

Opérations synchrones et opérations asynchrones

Les API Node.js fournissent des opérations asynchrones et synchrones pour certaines des mêmes opérations telles que les opérations de fichier. Bien qu’en règle générale, vous devez toujours penser de façon asynchrone en premier, vous pourriez parfois vouloir utiliser des opérations synchrones.

Par exemple, lorsqu’une interface de ligne de commande lit un fichier, puis utilise immédiatement les données dans le fichier. Dans ce cas, vous pouvez utiliser la version synchrone de l’opération de fichier, car aucun autre système ou personne n’attend d’utiliser l’application.

Toutefois, si vous créez un serveur web, vous devez toujours utiliser la version asynchrone de l’opération de fichier afin de ne pas bloquer la capacité d’exécution du thread unique à traiter les demandes d’autres utilisateurs.

Dans votre travail en tant que développeur chez TailWind Traders, vous devez comprendre la différence entre les opérations synchrones et asynchrones et quand utiliser chacune d’elles.

Performance par le biais d'opérations asynchrones

Node.js tire parti de la nature unique pilotée par les événements de JavaScript qui rend la composition des tâches serveur rapide et performante. JavaScript, lorsqu’il est utilisé correctement avec les techniques asynchrones, peut produire les mêmes résultats de performances que les langages de bas niveau tels que C, en raison des améliorations de performances rendues possibles par le moteur V8.

Les techniques asynchrones sont disponibles dans 3 styles, que vous devez être en mesure de reconnaître dans votre travail :

  • Async/await (recommandé) : Technique asynchrone la plus récente qui utilise les mots clés async et await pour recevoir les résultats d’une opération asynchrone. Async/await est utilisé dans de nombreux langages de programmation. En règle générale, les nouveaux projets avec des dépendances plus récentes utilisent ce style de code asynchrone.
  • Rappels : Technique asynchrone d’origine qui utilise une fonction de rappel pour recevoir les résultats d’une opération asynchrone. Vous verrez cela dans les bases de code et dans les API Node.js plus anciennes.
  • Promesses : Technique asynchrone plus récente qui utilise un objet de promesse pour recevoir les résultats d’une opération asynchrone. Vous verrez cela dans les bases de code et dans les API Node.js plus récentes. Vous devrez peut-être écrire du code basé sur les promesses dans votre travail pour envelopper des API plus anciennes qui ne seront pas mises à jour. En utilisant des promesses pour cet enveloppement, vous autorisez l’utilisation du code dans une plus grande gamme de projets avec version Node.js que dans le style de code async/await plus récent.

Async/await

Async/await est une nouvelle façon de gérer la programmation asynchrone. Async/await est le supplément syntaxique basé sur des promesses, qui rend le code asynchrone plus ressemblant au code synchrone. La lecture et la maintenance en sont également facilitées.

Le même exemple utilisant async/await ressemble à ceci :

// async/await asynchronous example

const fs = require('fs').promises;

const filePath = './file.txt';

// `async` before the parent function
async function readFileAsync() {
  try {
    // `await` before the async method
    const data = await fs.readFile(filePath, 'utf-8');
    console.log(data);
    console.log('Done!');
  } catch (error) {
    console.log('An error occurred...: ', error);
  }
}

readFileAsync()
  .then(() => {
    console.log('Success!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

Lorsque async/await a été publié dans ES2017, les mots-clés pouvaient uniquement être utilisés dans des fonctions et la fonction de niveau supérieur était une promesse. Si la promesse ne devait pas comporter de sections then et catch, elle devait néanmoins avoir une syntaxe promise pour pouvoir être exécutée.

Une fonction async renvoie toujours une promesse, même s'il n'y a pas d'appel await à l’intérieur. La promesse sera résolue avec la valeur retournée par la fonction. Si la fonction génère une erreur, la promesse sera rejetée avec la valeur générée.

Promise

Comme les rappels imbriqués peuvent être difficiles à lire et à gérer, Node.js a ajouté une prise en charge des promesses. Une promesse est un objet qui représente la réalisation (ou l’échec) d’une opération asynchrone.

Une fonction de promesse a le format suivant :

// Create a basic promise function
function promiseFunction() {
  return new Promise((resolve, reject) => {
    // do something

    if (error) {
      // indicate success
      reject(error);
    } else {
      // indicate error
      resolve(data);
    }
  });
}

// Call a basic promise function
promiseFunction()
  .then((data) => {
    // handle success
  })
  .catch((error) => {
    // handle error
  });

La méthode then est appelée lorsque la promesse est tenue, tandis que la méthode catch est appelée lorsque la promesse est rejetée.

Pour lire un fichier de manière asynchrone avec des promesses, le code est le suivant :

// promises asynchronous example

const fs = require('fs').promises;
const filePath = './file.txt';

// request to read a file
fs.readFile(filePath, 'utf-8')
  .then((data) => {
    console.log(data);
    console.log('Done!');
  })
  .catch((error) => {
    console.log('An error occurred...: ', error);
  });

console.log(`I'm the last line of the file!`);

Async/await de niveau supérieur

Les versions les plus récentes de Node.js ont ajouté un async/await de niveau supérieur pour les modules ES6. Vous devez ajouter une propriété nommée type dans le fichier package.json avec une valeur de module pour utiliser cette fonctionnalité.

{
    "type": "module"
}

Vous pouvez ensuite utiliser le mot clé await au niveau supérieur de votre code

// top-level async/await asynchronous example

const fs = require('fs').promises;

const filePath = './file.txt';

// `async` before the parent function
try {
  // `await` before the async method
  const data = await fs.readFile(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}
console.log("I'm the last line of the file!");

Rappels

Lors de la première publication de Node.js, la programmation asynchrone était gérée avec des fonctions de rappel. Les rappels sont des fonctions qui sont transmises en tant qu’arguments à d’autres fonctions. Une fois que la tâche a été effectuée, la fonction de rappel est appelée.

L’ordre des paramètres de la fonction est important. La fonction de rappel est le dernier paramètre de la fonction.

// Callback function is the last parameter
function(param1, param2, paramN, callback)

La fonction dans le code que vous conservez peut ne pas être appelée callback. Elle peut être appelée cb ou done ou next. Le nom de la fonction n’est pas important, mais l’ordre des paramètres est important.

Notez qu’il n’existe aucune indication syntaxique que la fonction est asynchrone. Vous devez savoir que la fonction est asynchrone en lisant la documentation ou en continuant à lire le code.

Exemple de rappel avec une fonction de rappel nommée

Le code suivant sépare la fonction asynchrone du rappel. Il est facile de lire et de comprendre et de réutiliser le rappel pour d’autres fonctions asynchrones.

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// callback
const callback = (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
};

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function
fs.readFile(filePath, 'utf-8', callback);

console.log("I'm the last line of the file!");

Le résultat correct est :

I'm the last line of the file!
Hi, developers!
Done!

Tout d’abord, la fonction asynchrone fs.readFile est démarrée et passe dans la boucle d’événements. Ensuite, l’exécution du code passe à la ligne de code suivante, qui est la dernière console.log. Une fois le fichier lu, la fonction de rappel est appelée et les deux instructions console.log sont exécutées.

Exemple de rappel avec une fonction anonyme

L’exemple suivant utilise une fonction de rappel anonyme, ce qui signifie que la fonction n’a pas de nom et ne peut pas être réutilisée par d’autres fonctions anonymes.

// callback asynchronous example

// file system module from Node.js
const fs = require('fs');

// relative path to file
const filePath = './file.txt';

// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function () => {}
fs.readFile(filePath, 'utf-8', (error, data) => {
  if (error) {
    console.log('An error occurred...: ', error);
  } else {
    console.log(data); // Hi, developers!
    console.log('Done!');
  }
});

console.log("I'm the last line of the file!");

Le résultat correct est :

I'm the last line of the file!
Hi, developers!
Done!

Lorsque le code est exécuté, la fonction asynchrone fs.readFile est démarrée et passe à la boucle d’événements. Ensuite, l’exécution continue à la ligne de code suivante, qui est la dernière console.log. Lorsque le fichier est lu, la fonction de rappel est appelée et les deux instructions console.log sont exécutées.

Rappels imbriqués

Étant donné que vous devrez peut-être appeler un rappel asynchrone ultérieur, puis un autre, le code de rappel peut devenir imbriqué. C’est ce que l’on appelle l’enfer de rappel, qui est difficile à lire et à maintenir.

// nested callback example

// file system module from Node.js
const fs = require('fs');

fs.readFile(param1, param2, (error, data) => {
  if (!error) {
    fs.writeFile(paramsWrite, (error, data) => {
      if (!error) {
        fs.readFile(paramsRead, (error, data) => {
          if (!error) {
            // do something
          }
        });
      }
    });
  }
});

API synchrones

Node.js a également un ensemble d’API synchrones. Ces API bloquent l’exécution du programme jusqu’à ce que la tâche soit terminée. Les API synchrones sont utiles lorsque vous souhaitez lire un fichier, puis utiliser immédiatement les données du fichier.

Les fonctions synchrones (bloquantes) dans Node.js utilisent la convention d’affectation de noms de functionSync. Par exemple, l’API asynchrone readFile a un équivalent synchrone appelé readFileSync. Il est important de respecter cette norme dans vos propres projets afin que votre code soit facile à lire et à comprendre.

// synchronous example

const fs = require('fs');

const filePath = './file.txt';

try {
  // request to read a file
  const data = fs.readFileSync(filePath, 'utf-8');
  console.log(data);
  console.log('Done!');
} catch (error) {
  console.log('An error occurred...: ', error);
}

En tant que nouveau développeur chez TailWind Traders, vous pouvez être invité à modifier n’importe quel type de code Node.js. Il est important de comprendre la différence entre les API synchrones et asynchrones et les différentes syntaxes du code asynchrone.