JavaScript, un langage single-thread

JavaScript, il faut le savoir, est un langage single-thread. Cela signifie que le code d’une page s’exécute sur un seul et unique thread, et qu’aucun appel n’est dispatché.

Prenons un exemple concret (on utilise jQuery pour simplifier) et essayons de déterminer dans quel ordre va s’exécuter le code suivant :

// 1. Code principal
console.log("Code principal");
var element = $("#mon-element");
element.on("click", onClick);

alert("Coucou!");

element.trigger("click");

someFunction();

// 2. Gestionnaire d'évènement
function onClick() {
  console.log("Gestionnaire d'évènement");
}

// 3. Méthode arbitraire
function someFunction() {
  console.log("Fonction arbitraire");
}

Et le résultat :

Capture console single-thread

On pouvait à priori s’y attendre, pourtant cet exemple mérite quelques commentaires.

Tout d’abord, l’appel à la fonction alert() a pour effet de bloquer l’exécution du code JavaScript. Tant que l’utilisateur n’aura pas cliqué sur le bouton de la fenêtre modale qui va s’afficher, le reste du code ne sera pas traité. On peut le voir facilement en mettant un point d’arrêt après la fonction.

Ensuite, il est intéressant de noter que le gestionnaire d’évènement sera exécuté dès l’appel à trigger(), et non pas mis en file d’attente comme on aurait pu le croire.

Une conséquence importante de la nature single-thread du JavaScript est que le code s’exécute sur le même thread que le thread graphique. Cela signifie que pendant ce temps l’interface utilisateur est bloquée (impossible de cliquer sur les boutons), et qu’aucun repaint de la page (rafraîchissement de l’affichage) ne peut être effectué.

Par exemple, si l’on essaie de faire tourner une boucle infinie, on constate qu’au bout de quelques secondes le navigateur nous propose d’arrêter le script qui selon lui « ne répond pas ». Tous les navigateurs modernes affichent ce comportement, qui vise à empêcher que des scripts malicieux ou mal conçus ne viennent perturber l’expérience utilisateur.

// Boucle infinie
while(true) {}

Capture firefox blocage

Mais nous allons voir maintenant que certaines fonctions peuvent faire varier l’ordre d’exécution du code JavaScript, et nous aider à résoudre les problèmes dus à des scripts trop longs à s’exécuter.

L’asynchronisme en JavaScript

Certaines tâches en JavaScript peuvent s’exécuter de manière asynchrone, c’est-à-dire de manière décalée par rapport au code principal. Cela peut être soit subi soit provoqué par le développeur lui-même.

Un exemple d’appel asynchrone subi est un appel Ajax. Lorsque l’on contacte le serveur pour récupérer des données, il est impossible de savoir combien de temps celui-ci mettra pour nous répondre, c’est pourquoi on indique une fonction de « callback » qui sera chargée de traiter le résultat retourné. Le navigateur va donc exécuter l’appel Ajax, attendre la réponse du serveur, puis exécuter le callback.

$.ajax("/UNE-URL-QUELCONQUE").done(function () {
  // Callback
});

// Ce code s'exécutera AVANT le callback
var toto = "toto";

Pendant le temps d’attente qui suit l’appel au serveur, deux faits remarquables vont se dérouler. Tout d’abord, le moteur JavaScript va traiter tout code en attente d’exécution, et notamment le code qui pourrait avoir été déclaré à la suite de l’appel Ajax. Ensuite, une fois que tout le code en attente aura été exécuté et si le serveur n’a toujours pas répondu, le moteur JavaScript rendra la main au thread graphique, permettant ainsi à l’utilisateur d’interagir normalement avec la page. Ce n’est que lorsque le serveur aura répondu que le thread graphique sera de nouveau interrompu afin de laisser le moteur JavaScript exécuter notre callback de traitement de la réponse.

Mais il est aussi possible de provoquer volontairement des appels asynchrones. Pour cela on utilise les méthodes JavaScript setTimeout() et setInterval(). Ces méthodes permettent respectivement de retarder l’exécution d’une fonction selon un temps d’attente défini, et de répéter l’exécution d’une fonction selon un intervalle de temps défini.

var timeoutId = setTimeout(function () {
  // Cette fonction s'exécutera dans 1 seconde (1000 millisecondes)
}, 1000);

Pendant le temps qu’attendra le navigateur avant d’exécuter le callback, l’interface utilisateur sera libérée. Ce sont ces fonctions que l’on utilise pour gérer l’affichage d’une animation (sur les vieux navigateurs) et surtout pour permettre au thread graphique de prendre une grande bouffée d’air avant de lancer une routine susceptible de mettre très longtemps à s’exécuter, anticipant ainsi l’affichage de l’avertissement « le script ne répond pas » que nous avons vu plus haut. Ces méthodes renvoient un identifiant unique (un nombre entier en fait) qui permet à tout moment d’annuler l’exécution retardée des callbacks avec respectivement les méthodes clearTimeout() et clearInterval().

// Finalement j'ai changé d'avis
// et je ne souhaite pas que le callback soit exécuté
clearTimeout(timeoutId);

Notez qu’Internet Explorer depuis la version 10 propose également la fonction setImmediate(), qui est l’équivalent d’un setTimeout() avec un délai d’attente de zéro. Son utilisation revient à dire au moteur JavaScript : termine de traiter tout ce qui est en attente, laisse le thread graphique faire un repaint, et ensuite seulement occupe-toi de ce bloc de code. Cette méthode, bien qu’elle n’ait pas été homologuée par le W3C, peut être très utile dans le cadre du développement d’applications Windows Store avec WinJS.

Au chapitre des fonctions introduisant de l’asynchronisme dans JavaScript, on peut aussi citer requestAnimationFrame(), une nouveauté HTML5 qui permet de demander au navigateur ne nous réserver une « fenêtre » avant le prochain repaint. C’est la méthode moderne pour qui veut gérer plus finement l’affichage de ses animations.

Les WebWorkers

Les WebWorkers sont une nouveauté HTML5 permettant d’exécuter du code JavaScript dans une tâche de fond, c’est-à-dire de faire du multi-thread. L’intérêt est de pouvoir effectuer côté client de lourds calculs dans un thread séparé, permettant ainsi de ne pas bloquer le thread graphique.

Pour cela, on doit déporter le code qui sera traité dans un fichier .js séparé. Le thread principal et le thread de tâche de fond que représente ce fichier pourront alors discuter par le biais d’une API de message.

// On créé un WebWorker en indiquant le fichier .js
var backgroundTask = new Worker("background-task.js");

// On s'abonne aux messages que va renvoyer le WebWorker
backgroundTask.addEventListener("message", function (event) {
  // Traitement des données renvoyées par le WebWorker
  var returnedData = event.data;
});

// On poste un message au WebWorker pour le lancer
backgroundTask.postMessage("");

Contenu du fichier « background-task.js » (le WebWorker) :

// Le fait de recevoir un message du code principal
// lance le traitement du WebWorker
onmessage = function (event) {
  var mainThreadData = event.data;

  // On simule une opération longue à s'exécuter
  setTimeout(
        function () {
            postMessage("Coucou du WebWorker!");
        },
        5000
    );
};

Le thread principal et le WebWorker peuvent s’échanger du JSON mais les données seront dupliquées. Comprendre : les modifier d’un côté ne les modifiera pas de l’autre, il faut les échanger grâce à l’API de message. Notez aussi que les WebWorkers reposent sur des threads gérés par le système d’exploitation lui-même.

Les WebWorkers ont toutefois une lourde limitation : ils s’exécutent dans une sorte de « bac à sable » conçu pour limiter les problèmes de sécurité, et à ce titre ils n’ont pas accès aux variables globales de l’objet window, notamment jQuery et document. Autrement dit les WebWorkers n’ont pas accès au DOM.

Ce dernier point peut s’avérer problématique, car JavaScript est souvent utilisé pour manipuler le DOM afin de modifier le rendu de la page web. Il est toutefois possible de résoudre cette difficulté en rusant. Par exemple, imaginons que nous ayons dans notre page un tableau contenant quelques centaines de lignes. Nous voulons trier celles-ci en fonction d’une donnée qu’elles contiennent afin de modifier ensuite leur ordre d’affichage. Le calcul permettant de faire le tri étant assez lourd, nous allons le déléguer à un WebWorker à qui nous enverrons un tableau contenant des paires « id de la ligne / donnée de tri ».

// On invoque un WebWorker et on lui envoie les données à trier
var tableSorter = new Worker("background-task.js");
tableSorter.postMessage([
  { "id": "line-1", "data": "Pierre" },
  { "id": "line-2", "data": "Paul" },
  { "id": "line-3", "data": "Jacques" }
]);

La responsabilité du WebWorker sera alors d’effectuer le plus gros du travail en triant le tableau, avant de renvoyer au thread principal un nouveau tableau contenant les ids dans l’ordre attendu. Il ne restera donc plus qu’à manipuler le DOM pour remettre les lignes dans le bon ordre.

// Le WebWorker se charge de l'opération la plus lourde : le tri
onmessage = function (event) {
  var tableData = event.data;
  tableData.sort(function (a, b) {
    if (a.data > b.data)
      return 1;

    if (a.data < b.data)
      return -1;

    return 0;
  });

  return tableData.map(function (line) {
    return line.id;
  });
};

Conclusion

Si vous voulez aller plus loin je vous invite à lire cette réponse sur StackOverflow, qui donne des détails intéressants sur l’aspect single-thread de JavaScript, notamment sur le fait qu’il est possible de « tricher » pour faire s’exécuter du code lorsque par exemple une fenêtre modale « alert » est affichée.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s