Le travail de développeur logiciel exige que nous apprenions des choses nouvelles et que nous suivions les tendances du métier. Que ce soit pour améliorer notre position sur le marché du travail ou simplement pour accroître nos connaissances et notre productivité. Au cours des deux dernières années, à ce que nous devons suivre se sont ajoutés d’abord les premiers signes de l’intelligence artificielle, sous la forme de divers assistants IA, de copilotes et d’intégrations de ChatGPT, capables de remplacer des processus courants et routiniers, mais aussi de générer des solutions à des problèmes plus complexes. Aujourd’hui, au moment où j’écris ce texte, les principaux modèles de langage sont capables de générer, à partir d’un seul prompt dans Claude Code Fable ou ChatGPT Astra, un projet entier qu’on peut déployer, tester et lancer. Du logiciel et de la médecine jusqu’au copywriting et à la traduction, les professionnels et les professionnelles de ces métiers font aujourd’hui face à des vagues de licenciements. De ce qu’on appelle le chômage technologique, on ne parle plus seulement dans le milieu universitaire. Il est devenu un sujet et une réalité quotidienne des métiers en col blanc, voir ici ou ici. Ce n’est ni la première ni la dernière fois que les progrès techniques rendent beaucoup d’emplois et de métiers inutiles et en créent, à l’inverse, de nouveaux. Dans ces cas-là, on sépare le bon grain de l’ivraie, et il est légitime de se demander : qui restera utile aux entreprises dans cette économie de marché AI-powered ?

Les postes juniors semblent être la première cible, et la plus naturelle. Des connaissances superficielles, une capacité encore peu développée à voir les problèmes techniques dans le contexte plus large des exigences techniques, mais aussi business. L’IA cesse de souffrir de ces défauts, alors que les postes juniors sont justement connus pour eux.

Si je regarde mon propre domaine, le front-end et les runtimes serveur du back-end node/deno/bun, il me semble qu’une connaissance approfondie de l’Event Loop est l’une des rares « hard skills » qui, dans l’univers de Javascript, séparent un codeur junior d’un ingénieur logiciel senior. Qui comprend l’Event Loop peut répondre à plusieurs questions importantes, par exemple : comment se fait-il que Javascript soit un langage de programmation single-threaded et que nous y fassions pourtant plusieurs choses à la fois au moyen d’opérations asynchrones ? Sur l’Event Loop, on peut écrire bien des choses. Mes ambitions sont modestes. Je vais regarder l’Event Loop de manière à pouvoir expliquer l’une des questions les plus courantes lors d’un entretien d’embauche pour un poste de développeur front-end : dans quel ordre les opérations suivantes s’exécutent-elles, et pourquoi :

setTimeout(() => {
  console.log('hey, from set timeout');
}, 0);

new Promise((resolve) => resolve('ahoy from promise')).then((v) =>
  console.log(v)
);

console.log('hello from console log');

Introduction technique au problème

Javascript est single-threaded. Si l’on ne compte pas la Web Workers API, cela signifie que Javascript ne sait traiter qu’une seule opération à la fois. Javascript est conçu de telle sorte qu’il traite les opérations séquentiellement, selon la méthode LIFO (Last-In, First Out), et tant qu’une opération n’est pas terminée, elle bloque toutes les opérations suivantes. En temps normal, par exemple, après un clic sur un bouton, une seule requête GET lente vers une API distante bloquerait toute interaction de l’utilisateur ; toute l’interface du navigateur serait figée jusqu’à ce que la requête réseau (network request) aboutisse. Quand Brendan Eich a conçu Javascript dans les années quatre-vingt-dix, le traitement des opérations y était synchrone, avec des possibilités limitées de traiter les événements déclenchés par l’interaction de l’utilisateur avec le navigateur. Pourquoi Eich a conçu Javascript ainsi mériterait une enquête historique. Aujourd’hui, en tout cas, nous savons que les pages web fondées sur Javascript (c’est-à-dire tout le web) ne se comportent pas ainsi. L’utilisateur peut interagir avec plusieurs parties d’une page web à la fois. Comment un langage de programmation single-threaded peut-il y parvenir ?

Enter Javascript runtime

Quand on dit Javascript, nous devons nous rendre compte qu’il s’agit d’une collection de bibliothèques et de composants. Ce n’est qu’en les reliant que nous obtenons un tout fonctionnel – le runtime, c’est-à-dire l’environnement dans lequel tourne notre application. Aujourd’hui, nous avons le choix entre deux grands types de runtime. Il s’agit soit du runtime d’un navigateur web, soit du runtime de l’une des diverses implémentations serveur, par exemple Node.js, Deno ou le tout récent Bun. Bien que le runtime des navigateurs et celui des serveurs aient des éléments communs, on y trouve aussi des différences importantes. Elles découlent de la vocation de chaque type de runtime. Les navigateurs web sont orientés vers le travail avec le DOM (window, document), les interactions de l’utilisateur (addEventListener), les opérations réseau au moyen de fetch ou XMLHttpRequest, le stockage local dans le navigateur (localStorage, sessionStorage), etc. Le runtime de Node.js ou de Bun, lui, est orienté vers l’environnement des applications serveur, le travail avec le système de fichiers, les bases de données, ou les opérations low-level sur le système d’exploitation. Mais les deux types de runtime implémentent l’Event Loop. Pour simplifier, je ne m’intéresserai dans la suite qu’au runtime des navigateurs web.

Le runtime des navigateurs web

Le runtime Javascript des navigateurs web se compose des parties suivantes :

  • Javascript Engine
  • Web APIs
  • Event Loop
  • Task queue et Microtask queue

L’Event Loop n’est que l’un des composants et n’entre en jeu que lors du traitement de code asynchrone. Si notre programme Javascript n’utilise que des opérations synchrones, l’Event Loop ne sera jamais sollicité. L’Event Loop compte pourtant parmi les caractéristiques majeures de Javascript, et c’est grâce à lui que les applications web modernes d’aujourd’hui sont pleines de fonctionnalités asynchrones. Pour comprendre l’Event Loop et, plus généralement, le traitement du code asynchrone en Javascript, il est bon de se rappeler comment Javascript se comporte lors d’opérations entièrement synchrones. Pour cela, il est bon de comprendre ensuite le fonctionnement du Javascript engine.

Javascript engine

Le cœur symbolique de Javascript, c’est l’engine. Les fonctions principales d’un JavaScript engine sont le parsing, l’interprétation et l’exécution du code. Par parsing du code, nous entendons la conversion du code source JavaScript en ce qu’on appelle un arbre syntaxique abstrait (AST), une structure que l’engine est capable de traiter. Par interprétation, nous entendons le fait que la plupart des engines modernes interprètent d’abord le code, puis le compilent en code machine (just-in-time compilation – JIT). Par exécution du code, enfin, nous entendons qu’après la traduction en code machine, l’engine commence à exécuter les instructions et assure le fonctionnement de l’application.

Exemples de JavaScript engines populaires :

  • V8 : utilisé par Google Chrome et Node.js. Il est connu pour sa rapidité et son efficacité.
  • SpiderMonkey : le premier JavaScript engine, développé par la société Netscape, aujourd’hui utilisé dans Firefox.
  • JavaScriptCore (aka Nitro) : utilisé par Safari et d’autres produits d’Apple.
  • Chakra : utilisé autrefois par Microsoft Edge (legacy), aujourd’hui remplacé par l’engine V8 dans le nouvel Edge (Chromium).

Le JavaScript engine fournit en outre deux structures de mémoire : le heap et le call stack. Leurs différences et leur fonctionnement mériteraient, eux aussi, un article à part sur le Memory management en Javascript. Pour une définition de base, lisez Wikipédia ou obtenez-la d’un prompt à ChatGPT.

Ce qui est important pour comprendre l’Event Loop, c’est que le Javascript Engine en tant que tel n’implémente pas le traitement du code asynchrone. Toute l’asynchronie de Javascript doit être implémentée ailleurs. Où donc ? C’est justement l’affaire du Javascript Runtime, qui ajoute au Javascript engine les Web APIs (nous sommes dans le navigateur), l’Event loop et les files d’attente Task queue et Microtask queue. Fun fact : si Javascript était un langage de programmation purement synchrone, il se contenterait du heap et du call stack et n’aurait pas besoin d’Event Loop. Mais j’anticipe.

Heap

Le heap, pour ce qui est de comprendre l’Event Loop, n’est pas si important. Le heap est une structure de mémoire où le Javascript Engine stocke les types de données complexes (Object, Array, Function, Map, Set, WeakMap, WeakSet). La taille du heap est donnée par la taille de la mémoire RAM. Contrairement au call stack, le heap n’est pas organisé de manière séquentielle : c’est une mémoire non linéaire. Dans le heap sont stockées des données dont le type et la taille ne sont pas connus à la compilation et peuvent changer pendant le runtime (affectation d’autres valeurs aux properties des objets, apparition de nouvelles properties, de nouveaux objets, etc.) ; le heap est donc une mémoire dynamique. Techniquement parlant, le heap est une implémentation du patron de conception courant de l’allocation dynamique de la mémoire (dynamic memory allocation pattern), ce qui signifie qu’il exige un Garbage Collector, qui nettoie la mémoire allouée pendant le runtime selon qu’une partie du programme a encore besoin (a des références dans le call stack) des objets alloués dans le heap. Si ce n’est pas le cas, le Garbage Collector, en temps normal, nettoie le heap et libère la mémoire. Le Garbage Collector n’est cependant pas précis à 100 %, si bien que, malgré son existence, des fuites de mémoire (memory leaks) peuvent toujours se produire.

Call Stack

D’un autre côté, le call stack est une mémoire statique plus simple, et bien plus intéressante pour expliquer l’Event Loop, car elle a à voir avec l’ordre dans lequel le code s’exécute en Javascript. Le Javascript engine travaille avec le call stack de manière uniquement synchrone, pas à pas. Sur le plan technique, le call stack est une implémentation du patron de conception classique de la Pile (Stack), où le dernier élément ajouté en mémoire est traité le premier (Last In, First Out, LIFO). Et qu’est-ce qui est exactement stocké dans le call stack, et de quoi détermine-t-il l’ordre au juste ? Pour simplifier, on peut se représenter un programme Javascript comme une collection de fonctions et de variables, et le call stack veille à ce que nos fonctions soient appelées et nos variables déclarées dans le bon ordre et au bon moment.

Alors que les types de données complexes (objets, tableaux, etc.) sont stockés dans le heap, le call stack ne contient que des références/pointeurs vers le heap. Ce qui, à l’inverse, est réellement stocké dans le call stack, ce sont les valeurs des types de données primitifs (string, integer, boolean, etc.), mais aussi tout le contexte d’exécution d’une fonction, par quoi l’on entend tout « ce qui est nécessaire pour appeler la fonction ». Là encore, il conviendrait d’approfondir tout ce que recouvre le contexte d’exécution d’une fonction, mais, pour simplifier, le contexte d’exécution comprend :

  1. Variable Environment (environnement des variables) : un enregistrement des variables et des déclarations de fonctions propres à la fonction donnée.
  2. Lexical Environment (environnement lexical) : des références au scope parent (environnement lexical) et les variables définies avec let et const.
  3. La liaison de this : la valeur de this dans la fonction en cours.
  4. Les détails propres à l’appel : les arguments passés à la fonction et le mécanisme de son retour (c’est-à-dire l’endroit du programme où celui-ci doit revenir une fois la fonction terminée).

Si je laisse Claude générer un exemple au hasard pour montrer comment fonctionne le call stack, prenons par exemple le code suivant :

function multiply(x, y) {
  const result = x * y;
  return result;
}

function calculate() {
  const a = 10;
  const b = 20;
  const product = multiply(a, b);
  console.log(product);
}

calculate();

Que se passe-t-il exactement dans un exemple aussi simple ? La situation est la suivante : dans le call stack est d’abord placée la fonction calculate(), ou plutôt tout son contexte d’exécution. Ensuite, le contexte d’exécution de la fonction multiply() est ajouté au call stack. Dès que la fonction multiply() a été appelée et son résultat affecté à la variable product, la fonction multiply() et son contexte d’exécution sont retirés du call stack. Puis le contexte d’exécution de console.log() est placé dans le call stack. Après l’affichage de la valeur « 200 » dans la console du navigateur, le contexte d’exécution de console.log() est retiré du call stack. Enfin, le contexte d’exécution de la fonction calculate() est retiré lui aussi.

Remarquons que l’exemple ne contient pas de variables globales et que l’appel de la fonction calculate() ne se trouve, en apparence, dans aucune fonction dont le contexte d’exécution pourrait être ajouté au call stack. Comment, alors, les variables globales et les appels de fonctions dans le scope global parviennent-ils dans le call stack ?

Global Execution Context (GEC)

Tout ce qui se trouve dans le scope global peut être compris comme appelé à l’intérieur d’une fonction principale, dont le contexte d’exécution a un nom et un comportement particuliers : le global execution context. Modifions l’exemple utilisé plus haut et ajoutons-y des variables globales.

var globalA = 'testGlobalA';
const globalB = 'testGlobalB';

function multiply(x, y) {
  const result = x * y;
  return result;
}

function calculate() {
  const a = 10;
  const b = 20;
  const product = multiply(a, b);
  console.log(product);
}

calculate();

Si nous admettons que le contexte global de nos scripts est traité comme une sorte de fonction « main », notre script se comportera de la manière suivante : le contexte d’exécution global est ajouté au call stack, la variable « globalA » est ajoutée à l’environnement des variables, la variable « globalB » est ajoutée à l’environnement lexical, et une liaison de this est créée, qui, dans l’environnement du navigateur, renverra à l’objet global window. En ce qui concerne l’objet window, il est bon de rappeler que, si var globalA est ajoutée comme property de l’objet window, les variables définies avec const ou let sont ajoutées au scope global/à l’environnement lexical global, mais non à l’objet global window.

Dès que toutes les fonctions définies « à l’intérieur » du contexte d’exécution global ont été ajoutées au call stack, traitées et retirées du call stack, il ne reste dans le call stack que le contexte d’exécution global, en tant que dernière fonction, et celle-ci est finalement retirée à son tour du call stack, qui se retrouve vide.

Voilà du moins comment cela fonctionne en principe, si tout le code que traite le Javascript engine est synchrone. Si notre application (lisez : à l’intérieur du contexte d’exécution global) contient des fonctions traitées de manière asynchrone, la situation est sensiblement différente.

Nous avons dit que le global execution context (GEC) est une sorte de fonction « main » initiale et le point d’entrée de l’application. Il semble qu’il doive tourner tout le temps, tant que tout le code n’a pas été traité. Mais qu’en est-il des parties asynchrones du code ? Elles peuvent bien lancer d’autres fonctions, qui renvoient de nouvelles valeurs et créent de nouveaux contextes d’exécution, si bien que le contexte d’exécution global doit être « d’une manière ou d’une autre » présent dans le call stack. Et comment et quand le code asynchrone est-il appelé, lorsque le call stack est partiellement rempli, ou lorsqu’il est entièrement vide ? Et si le call stack est vide, comment est-il possible que quelque chose se lance en Javascript, par exemple quand l’utilisateur clique sur un bouton et que se déclenche le callback d’un event listener enregistré au moyen d’addEventListener() ? Et qu’est-ce qui surveille les callbacks asynchrones d’addEventListener, alors que le Javascript engine est entièrement synchrone, et qu’il ne contient même aucune fonction addEventListener ?

L’asynchronie en Javascript, ou Web APIs, Task queues, Event loop

L’engine du langage de programmation Javascript ne contient ni l’objet console ni sa méthode console.log(). Ce qui peut surprendre. Le fait est que des fonctionnalités courantes pour les programmeurs, comme setTimeout, addEventListener, les opérations de manipulation du DOM, les requêtes réseau asynchrones fetch ou XMLHttpRequest, font partie non pas du Javascript engine, mais d’un autre composant du Javascript runtime : la Web API.

Web API

Comment exactement le Javascript engine parvient-il à coopérer avec la Web API de façon à avoir accès aux fonctions console.log() ou setTimeout(), si elles ne font pas directement partie de l’engine ? Le Javascript Engine (par ex. V8) possède un registre interne des fonctions natives, définies dans la spécification ECMAScript (par ex. Math.random(), Array.prototype.map(), Date.now()). C’est ainsi que l’engine reconnaît que setTimeout() n’est pas une fonction native ; il va donc chercher dans le scope global, ou plus précisément dans l’objet window, et tente d’appeler la méthode window.setTimeout(). Le Javascript runtime veille en effet, avant même l’initialisation du programme, à ce que le contexte d’exécution global (et donc l’environnement lexical de chaque fonction, voir plus haut) contienne les objets qui font partie de la Web API, par ex. document, navigator, performance, Intl, crypto, console.

Quand j’ai demandé à ChatGPT de m’expliquer le processus d’exécution de la méthode console.log(), il m’a écrit, à tort, que console.log() fait partie du Javascript engine lui-même. Quand je lui ai fait remarquer que ce n’était pas le cas, ChatGPT s’est excusé et m’a proposé une réponse plus exacte. Je voulais savoir pourquoi il avait fait cette erreur. Il l’a mise sur le compte d’un modèle mental du fonctionnement de Javascript, prétendument répandu parmi les développeurs (et donc aussi parmi les blogueurs), selon lequel console.log() est une opération synchrone, donc traitée directement sur le call stack (ce qui est vrai), d’où il découlerait que si quelque chose est traité directement sur le call stack (et n’a pas besoin de callbacks traités de manière asynchrone en dehors du thread principal de Javascript, ni donc de l’Event loop), cela doit faire partie du Javascript engine.

C’est une erreur. La méthode console.log()n’existe pas dans le Javascript engine, celui-ci n’en sait rien, sinon qu’elle n’est pas une fonction native. Il en délègue le traitement concret au Javascript runtime, où le navigateur (ou nodejs) exécute ensuite la fonction au moyen de code bas niveau en langage C++, via des bindings C++, c’est-à-dire des liaisons entre une fonction Javascript et une fonction C++ qui accomplit réellement ce que nous demandons à la fonction Javascript.

Nous savons maintenant, en gros, comment cela fonctionne quand le Javascript engine doit appeler une fonction non native de la Web API. Mais qu’en est-il de l’asynchronie et de l’appel de setTimeout(), addEventListener() ou fetch() ?

Callbacks et Task Queues en général

En tant que langage single-threaded, le Javascript engine doit déléguer l’exécution des opérations asynchrones hors de lui-même. Outre la Web API, le Javascript runtime contient d’autres composants qui rendent les opérations asynchrones possibles : les Task queues et l’Event loop. Toutes les opérations asynchrones en Javascript sont réalisées au moyen de callbacks (y compris await/async, qui sont du sugar code reposant sur l’asynchronie fondée sur les Promises). Lorsque l’engine rencontre une opération asynchrone, il en confie entièrement le traitement à la Web API. S’il s’agit d’un minuteur comme setTimeout(), la Web API attend le nombre de millisecondes indiqué ; s’il s’agit d’une requête réseau comme fetch, c’est encore à la Web API d’attendre la réponse à cette requête. Dès que la Web API a terminé le traitement de l’opération asynchrone elle-même, elle prend le callback défini par le programmeur et transmet cette fonction de callback à un composant particulier du Javascript runtime, une file appelée Task queue ou Microtask queue.

Les callbacks restent dans les files tant que le call stack n’est pas entièrement vide, ou plus précisément tant qu’il n’y reste pas uniquement le contexte d’exécution global (GEC). Dès qu’il n’y a plus que le GEC dans le call stack, c’est au tour d’un autre composant : l’Event Loop.

Task queue

La Task queue est la plus ancienne des deux files et, en un sens, elle est aussi ancienne que Javascript lui-même. Dès les premières versions de Javascript dans Netscape Navigator, on disposait de setTimeout() et de la réaction aux événements déclenchés par l’utilisateur, et l’un comme l’autre avaient besoin d’un endroit où le callback attend que l’engine ait terminé ce qu’il est en train de faire. Formellement, cependant, la Task queue (et l’Event loop en général) n’a été décrite que par la spécification HTML5 de l’organisation WHATWG, qui a unifié la manière dont tous les navigateurs doivent se comporter à cet égard.

La raison d’être de la Task queue est celle par laquelle nous avons commencé : un engine single-threaded ne doit rien attendre. La Web API gère donc l’attente hors de l’engine et ne transmet à la Task queue que le callback prêt, qui sera traité quand son tour viendra. Dans la Task queue (la spécification HTML les appelle tasks ; dans les blogs, on rencontre souvent aussi le terme macrotasks) se trouvent :

  • les callbacks des minuteurs setTimeout() et setInterval(),
  • les callbacks des événements enregistrés au moyen d’addEventListener() (clic, frappe d’une touche, scroll…),
  • les événements réseau des anciennes API, par exemple onload de XMLHttpRequest ou les messages de WebSocket,
  • les messages entre fenêtres et workers envoyés au moyen de postMessage(),
  • et, d’une manière générale, le lancement même du script depuis la balise <script> – notre contexte d’exécution global commence donc lui aussi sa vie comme une task.

Deux choses méritent d’être mentionnées. Premièrement, « Task queue » au singulier est une simplification. La spécification autorise le navigateur à avoir plusieurs files (par ex. une pour les interactions de l’utilisateur et une autre pour les minuteurs) et à choisir entre elles, si bien que le navigateur peut donner la priorité à un clic sur un minuteur. Au sein d’une même file, en revanche, c’est la méthode FIFO qui s’applique. Deuxièmement, l’Event loop ne prend jamais qu’une seule task dans la Task queue, et entre deux tasks, le navigateur a l’occasion de redessiner la page. C’est pourquoi même setTimeout(callback, 0) ne signifie pas « tout de suite », mais « au plus tôt au prochain tour de l’Event loop ». De plus, lors d’appels de minuteurs profondément imbriqués, les navigateurs portent le délai minimal à 4 ms.

Microtask queue

La Microtask queue est bien plus jeune. La notion de microtask est apparue vers 2011, lorsque des ingénieurs de Mozilla et de Chromium proposaient un remplacement de ce qu’on appelait les Mutation Events – un ancien mécanisme qui permettait de suivre les changements dans le DOM. Les Mutation Events se déclenchaient de manière synchrone à chaque changement du DOM, ce qui était lent et sujet aux erreurs. La nouvelle API MutationObserver devait livrer les changements de manière asynchrone et tous ensemble, en un lot, mais en même temps avant que le navigateur ne redessine la page ou ne traite l’événement suivant. La Task queue n’y suffisait pas, car entre deux tasks, un redessin ou le traitement d’un autre clic peut déjà avoir lieu. C’est pourquoi est née une nouvelle file, dont les callbacks sont traités dès que le call stack se vide, avant même la task suivante.

Le deuxième usage de la Microtask queue, aujourd’hui le plus connu, ce sont les Promises, standardisées dans ECMAScript 2015 (ES6). La spécification ECMAScript les appelle « jobs », mais le navigateur les place justement dans la Microtask queue. Les Promises ont ainsi obtenu une garantie importante : le callback de .then() n’est jamais appelé de manière synchrone, mais jamais inutilement tard non plus – toujours juste après la fin du code en cours d’exécution. Vers 2019, les navigateurs ont ensuite ajouté la fonction queueMicrotask(), grâce à laquelle le programmeur peut lui aussi planifier directement une microtask, sans passer par une Promise.

Dans la Microtask queue se trouvent :

  • les callbacks des Promises : .then(), .catch(), .finally(),
  • la suite d’une fonction async après await (async/await est construit sur les Promises),
  • les callbacks passés à queueMicrotask(),
  • les callbacks de MutationObserver.

Et encore une règle importante : alors que l’Event loop ne prend dans la Task queue qu’une seule task à un moment donné, il vide toujours la Microtask queue tout entière, y compris les microtasks qui s’y sont ajoutées pendant son traitement. Si une microtask ajoutait sans cesse une autre microtask, le navigateur n’arriverait plus ni au clic ni au redessin de la page, et la page se figerait – exactement comme lors d’une boucle infinie dans du code synchrone.

Event Loop

Le mystérieux Event loop va maintenant se révéler comme quelque chose de relativement trivial. L’Event loop n’est rien d’autre qu’un composant du Javascript runtime qui vérifie sans cesse si le call stack est plein. S’il est vide, la tâche suivante de l’Event loop est de décider s’il faut prendre, selon la méthode FIFO, le premier callback de la Task queue ou de la Microtask queue. Voilà tout.

Le script s’exécute dans le contexte d’exécution global. setTimeout confie un minuteur à la Web API et, après 0 ms, son callback est placé dans la Task queue. Le callback de .then() de la Promise tenue est placé dans la Microtask queue. console.log s’exécute de façon synchrone et écrit dans la console « hello from console log ». Quand le call stack se vide, l’Event Loop traite d’abord la Microtask queue et écrit « ahoy from promise », et ce n’est qu’ensuite qu’il prend la task de la Task queue et écrit « hey, from set timeout ». setTimeout(() => { console.log('hey, from set timeout'); }, 0); new Promise((resolve) => resolve('ahoy from promise')) .then((v) => console.log(v)); console.log('hello from console log'); Call stack Web APIs Event Loop stack vide ? → d’abord les microtasks, puis une task Microtask queue Promise, queueMicrotask, MutationObserver Task queue setTimeout, événements, opérations réseau Elements Console Sources Network top ▾ Filter Default levels ▾ hello from console logscript.js:6 ahoy from promisescript.js:5 hey, from set timeoutscript.js:2 › global execution context setTimeout(…, 0) minuteur : 0 ms new Promise(…).then(…) console.log('hello…') console.log(v) console.log('hey…') (v) => console.log(v) () => { console.log(…) } 1. Le script démarre comme une task. Dans le call stackentre le contexte d’exécution global. 2. setTimeout() confie le minuteur à la Web API. Après0 ms, son callback est placé dans la Task queue. 3. La Promise est tenue aussitôt, donc le callbackde .then() part dans la Microtask queue. 4. console.log() s’exécute de façon synchrone sur le callstack et écrit la première ligne dans la console. 5. Le script est terminé et le call stack est vide.L’Event Loop entre en jeu. 6. L’Event Loop vide d’abord la Microtask queue :le callback de .then() écrit « ahoy from promise ». 7. La Microtask queue est vide ; l’Event Loop prenddonc la première task : le callback de setTimeout(). Résultat : « hello », « ahoy », « hey » – d’abord le codesynchrone, puis les microtasks, et ensuite les tasks. Rejouer Pause Reprendre