Introduction aux workers
========================

* Précédent  (en-US)
* Aperçu : Asynchronous
* Suivant  (en-US)

Dans ce dernier article de notre module sur le JavaScript asynchrone, nous présenterons les *workers* (qu'on pourrait traduire en français par *travailleurs*, moins usité), qui permettent d'exécuter certaines tâches dans un fil d'exécution séparé.

|  |  |
| --- | --- |
| Prérequis : | Notions informatiques de base, compréhension raisonnable des fondamentaux de JavaScript, notamment des fonctions et des gestionnaires d'évènements. |
| Objectif : | Comprendre comment utiliser les *web workers*. |

Dans le premier article de ce module, nous avons vu ce qui se passe lorsque vous avez une tâche synchrone de longue durée dans votre programme : c'est toute la fenêtre ne répond plus. La raison principale de ce problème est l'exécution du programme au sein d'*un seul fil d'exécution* (*thread*). Un *fil d'exécution* est une séquence d'instructions suivie par un programme. Parce que le programme s'exécute sur un seul fil, il ne peut faire qu'une seule chose à la fois : il attend donc la réponse de notre appel synchrone de longue durée et ne peut rien faire d'autre.

Les *workers* vous donnent la possibilité d'exécuter certaines tâches dans un fil d'exécution différent. Vous pouvez donc démarrer la tâche, puis continuer avec d'autres traitements (comme la gestion des actions de l'utilisateur).

Mais il y a un prix à payer pour cela. Avec le code parallélisé, vous ne savez jamais quand votre fil sera suspendu et quand l'autre fil aura une chance de s'exécuter. Ainsi, si les deux fils ont accès aux mêmes variables, il est possible qu'une variable change de manière inattendue à tout moment, ce qui provoque des bogues difficiles à trouver.

Pour éviter ces problèmes sur le Web, le code du fil principal et le code qui s'exécute dans le *worker* n'accèdent jamais directement aux variables de l'autre. Le code des *workers* et le code principal s'exécutent dans des mondes complètement séparés et n'interagissent qu'en s'envoyant des messages. Cela signifie notamment que les *workers* ne peuvent pas accéder au DOM (la fenêtre, le document, les éléments de la page, etc.).

Il existe trois types de *workers* :

* Les *workers* dédiés
* Les *workers* partagés
* Les *service workers*

Dans cet article, nous allons voir un exemple décrivant le premier type, puis discuter brièvement des deux autres.

Utiliser les <i lang="en>web workers
------------------------------------

Vous souvenez-vous du premier article où nous avions une page qui calculait les nombres premiers ? Nous allons ici utiliser un *worker* pour calculer les nombres premiers afin que notre page reste réactive aux actions des utilisatrices et utilisateurs.

### Le générateur de nombres premiers synchrone

Revoyons d'abord le code JavaScript de notre exemple précédent :


```js
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() \* (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  return primes;
}

document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  const primes = generatePrimes(quota);
  document.querySelector("#output").textContent =
    `Génération de ${quota} nombres premiers terminée !`;
});

document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    'Essayez de taper ici immédiatement après avoir appuyé sur "Générer des nombres premiers"';
  document.location.reload();
});
```


Avec ce programme, après avoir appelé `generatePrimes()`, le navigateur ne répond plus du tout.

### Génération de nombres premiers avec un *worker*

Pour cet exemple, commencez par faire une copie locale des fichiers présents dans https://github.com/mdn/learning-area/blob/main/javascript/asynchronous/workers/start. Quatre fichiers sont dans ce répertoire :

* `index.html`
* `style.css`
* `main.js`
* `generate.js`

Le fichier `index.html` et les fichiers `style.css` sont déjà complets :


```html
<!doctype html>
<html lang="fr-FR">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Nombres premiers</title>
    <script src="main.js" defer></script>
    <link href="style.css" rel="stylesheet" />
  </head>

  <body>
    <label for="quota">Quantité de nombres premiers à générer :</label>
    <input type="text" id="quota" name="quota" value="1000000" />

    <button id="generate">Générer des nombres premiers</button>
    <button id="reload">Recharger</button>

    <textarea id="user-input" rows="5" cols="62">
Essayez de taper ici immédiatement après avoir appuyé sur "Générer des nombres premiers"</textarea
 >

    <div id="output"></div>
  </body>
</html>
```



```css
textarea {
  display: block;
  margin: 1rem 0;
}
```


Les fichiers `main.js` et `generate.js` sont vides. Nous allons ajouter le code s'exécutant dans le fil principal à `main.js` d'une part et celui qui s'exécute via le *worker* à `generate.js` d'autre part.

Tout d'abord, nous pouvons voir que le code du *worker* est présent dans un fichier distinct du code principal. En regardant `index.html` ci-dessus, nous pouvons également observer que seul le code principal est inclus dans un élément `<script>`.

Copiez maintenant le code suivant dans `main.js` :


```js
// On crée un nouveau worker en lui injectant le code présent dans le fichier "generate.js"
const worker = new Worker("./generate.js");

// Lorsque la personne clique sur "Générer des nombres premiers", on envoie un message au worker.
// La commande portée par le message est "generate", et le message contient également "quota"
// qui indique la quantité de nombres premiers à générer.
document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  worker.postMessage({
    command: "generate",
    quota,
  });
});

// Lorsque le worker renvoie un message au fil principal. Grâce aux données
// du message, on met à jour la zone de sortie avec un texte, indiquant aussi
// le quantité de nombres premiers générés.
worker.addEventListener("message", (message) => {
  document.querySelector("#output").textContent =
    `Génération de ${message.data} nombres premiers terminée !`;
});

document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    'Essayez de taper ici immédiatement après avoir appuyé sur "Générer des nombres premiers"';
  document.location.reload();
});
```


1. Tout d'abord, nous créons le *worker* en utilisant le constructeur `Worker()`. Nous lui passons un lien pointant vers le script du *worker*. Dès que le *worker* est créé, le script correspondant est exécuté.
2. Ensuite, comme dans la version synchrone, nous ajoutons un gestionnaire d'événements `click` au bouton "Générer des nombres premiers". En revanche, plutôt que d'appeler une fonction `generatePrimes()`, nous envoyons un message au travailleur en utilisant `postMessage()`. L'argument de cette fonction est le message à transmettre. Ici nous transmettons un objet JSON contenant deux propriétés :

`commande`

Une chaîne de caractères indiquant la tâche demandée au *worker* (au cas où notre *worker* pourrait faire plus d'une chose)

`quota`

La quantité de nombres premiers à générer.
3. Ensuite, nous ajoutons un gestionnaire d'évènements `message` au *worker*. C'est ainsi qu'il peut nous dire quand il a terminé et transmettre les données résultantes. Notre gestionnaire prend les données de la propriété `data` du message et les écrit dans l'élément de sortie (les données sont exactement les mêmes que `quota`, donc c'est un peu inutile, mais cela illustre le principe).
4. Enfin, nous implémentons le gestionnaire d'événements `click` pour le bouton "Recharger". C'est la même chose que dans la version synchrone.

Passons maintenant au code du *worker*. Copiez le code suivant dans `generate.js` :


```js
// On écoute les messages du fil principal.
// Si la commande de message est "generate", on appelle `generatePrimes()`
addEventListener("message", (message) => {
  if (message.data.command === "generate") {
    generatePrimes(message.data.quota);
  }
});

// On génère des nombres premiers (très inefficacement)
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() \* (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  // Lorsque c'est terminé, on envoie un message au fil principal
  // incluant la quantité de nombres premiers générés.
  postMessage(primes.length);
}
```


N'oubliez pas que ce code s'exécute dès que le script principal crée le *worker*.

Le *worker* commence par écouter les messages provenant du script principal. Il le fait en utilisant `addEventListener()`, qui est une fonction globale dans un *worker*. À l'intérieur du gestionnaire d'évènements `message`, la propriété `data` de l'évènement contient une copie de l'argument transmis par le script principal. Si le script principal a passé la commande `generate`, nous appelons `generatePrimes()`, en transmettant la valeur `quota` des données de l'évènement `message`.

La fonction `generatePrimes()` utilise le même algorithme que la version synchrone, sauf qu'au lieu de renvoyer une valeur, nous envoyons un message au script principal lorsque le calcul est terminé. Nous utilisons la fonction `postMessage()` (en-US) pour cela. Comme `addEventListener()`, il s'agit d'une fonction globale dans le contexte d'un *worker*. Comme nous l'avons déjà vu, le script principal écoute ce message et mettra à jour le DOM lorsque le message sera reçu.

**Note :** Pour exécuter ce site, vous devrez exécuter un serveur web local, car les URL de type `file://` ne sont pas autorisées à charger des *workers*. Consultez notre guide pour configurer un serveur de test local. Une fois que cela aura été fait, vous devriez pouvoir cliquer sur "Générer des nombres premiers" et faire en sorte que votre page principale reste réactive.

Si vous rencontrez des problèmes lors de la création ou de l'exécution de l'exemple, vous pouvez voir la version finale sur https://github.com/mdn/learning-area/blob/main/javascript/asynchronous/workers/finished et l'essayer en direct sur https://mdn.github.io/learning-area/javascript/asynchronous/workers/finished.

Les autres types de *workers*
-----------------------------

Le *worker* que nous venons de créer était ce qu'on appelle un **worker* dédié* (*dedicated worker* en anglais). Cela signifie qu'il est utilisé par une seule instance de script.

Il existe cependant d'autres types de *workers* :

* Les *workers* partagés qui peuvent être partagés par plusieurs scripts différents s'exécutant dans différentes fenêtres.
* Les *qui agissent comme des serveurs intermédiaires, mettant en cache les ressources afin que les applications web puissent fonctionner même hors ligne. Il s'agit d'un élément clé des applications web progressives (PWA).*

*Conclusion
----------*Dans cet article, nous avons présenté les *web workers*, qui permettent à une application web de décharger des tâches sur un fil d'exécution séparé. Le fil d'exécution principal et le *worker* ne partagent pas de variables directement, mais communiquent avec des messages, reçus par l'autre côté en tant qu'évènements `message`.

Bien qu'ils ne puissent pas utiliser toutes les API auxquelles le document a accès (le DOM notamment), les *workers* peuvent être un moyen efficace de garder l'application principale réactive.

Voir aussi
----------

* Utiliser les *web workers*
* Utiliser les *service workers*
* L'API *Web Workers*
* Précédent  (en-US)
* Aperçu : Asynchronous
* Suivant  (en-US)