JavaScript gère la concurrence grâce à une « boucle d'événements ». Ce modèle est différent de la gestion faite par des langages comme C ou Java.
Notions liées à l'exécution
Les sections qui suivent décrivent un modèle théorique. En réalité, les moteurs JavaScript implémentent et optimisent fortement la sémantique décrite ici.
Représentation visuelle
La pile d'appels (stack)
Les appels de fonction forment une pile de cadre (frames).
function f(b){ var a = 12; return a+b+35; } function g(x){ var m = 4; return f(m*x); } g(21);
Lors de l'appel à g
, on crée un premier cadre contenant les arguments de g
ainsi que les variables locales. Quand g
appelle f
, un deuxième cadre est créé et placé sur le premier et contient les arguments de f
ainsi que les variables locales. Lorsque f
a fini et renvoyé son résultat, le cadre correspondant (celui qui est sur le dessus) est retiré de la pile (il reste donc le cadre lié à l'appel de g
). Quand g
a fini grâce aux informations transmises, la pile devient vide.
Le tas (heap)
Les objets sont alloués en mémoire dans un tas qui désigne une zone de la mémoire sans structure particulière.
La file (queue)
Un environnement d'exécution JavaScript (runtime) contient une queue de messages à traiter. Chaque message est associé à une fonction. Lorsque la pile est vide, on retire un message de la queue et on le traite. Le traitement consiste à appeler la fonction associée au message (et donc à créer le cadre dans la pile d'appel). Le traitement d'un message est fini lorsque la pile d'appels redevient vide.
La boucle d'événements
La boucle d'événement tire principalement son nom de son implémentation. Celle-ci ressemble à :
while(queue.attendreMessage()){ queue.traiterProchainMessage(); }
queue.attendreMessage
est un fonction synchrone qui attend un message même s'il n'y en a aucun à traiter.
Traiter de A à Z (run-to-completion)
Chaque message sera traité complètement avant tout autre message. Cela permet de savoir que, lorsqu'une fonction s'exécute, on ne peut pas observer l'exécution d'un autre code qui prendrait le pas (modifiant les données de la fonction par exemple). Le modèle de thread utilisé par le langage C, par exemple, permet que du code soit exécuté après avoir stoppé une fonction d'un thread.
Ce modèle possède un désavantage : lorsqu'un message prend trop de temps à être traité, l'application ne peut plus gérer les interactions utilisateur comme les clics ou le défilement. Généralement, le navigateur affiche alors « Le script met trop de temps à répondre ». Une bonne pratique consiste à rendre le traîtement de message le plus court possible et à découper le message problématique en plusieurs messages.
L'ajout de messages
Dans les navigateurs web, des messages sont ajoutés à chaque fois qu'un événement se produit et qu'un gestionnaire d'événements y est attaché. S'il n'y a pas d'écouteur (listener) pour intercepter l'événement, il est perdu. Ainsi, si on clique un élément qui possède un gestionnaire d'événements pour les clics, un message sera ajouté (il en va de même avec les autres événements).
L'appel de setTimeout
enverra un message à la suite de la queue une fois que la durée passée en argument est écoulée. S'il n'y a pas d'autre message dans la queue, le message sera traîté directement. En revanche, s'il y a d'autres messages auparavant, le message de setTimeout
devra attendre la fin du traîtement des messages précédents. C'est pourquoi le deuxième argument de cette fonction indique une durée minimum et non une durée garantie.
Attention : L'argument passé pour le délai à setTimeout
ne correspond pas au temps exact. Cela correspond au délai minimum et non à un délai garanti. Par exemple setTimeout(maFonction(),100);
indique uniquement que maFonction
sera lancé au moins après 100 millisecondes.
La communication entre plusieurs environnements d'exécution
Un web worker ou une iframe
d'origine multiple (cross origin) possède sa propre pile, son propre tas et sa propre queue de messages. Deux environnements d'exécution distincts peuvent uniquement communiquer via des messages envoyés par la méthode postMessage
. Cette méthode permet d'ajouter un message à un autre environnement d'exécution si celui-ci « écoute » les événements message
.
Non bloquant
Le modèle de la boucle d'événement possède une caractéristique intéressante : JavaScript, à la différence d'autres langages, ne bloque jamais. La gestion des entrées-sorties (I/O) est généralement traitée par des événements et des callbacks. Ainsi, quand l'application attend le résultat d'une requête IndexedDB ou d'une requête XHR, il reste possible de traiter d'autres éléments comme les saisies utilisateur.
Il existe certaines exceptions historiques comme alert
ou des appels XHR synchrones. C'est une bonne pratique que de les éviter. Attention, certaines exceptions existent (mais relèvent généralement de bugs d'implémentation).