Accueil / Articles / Aperçu ou valeur en temps réel ? Comment déterminer ce que voient les appels de fonction JavaScript différés

Aperçu ou valeur en temps réel ? Comment déterminer ce que voient les appels de fonction JavaScript différés

Découvrez pourquoi les closures conservent un accès aux bindings plutôt qu’à des copies, et comment choisir entre des données en snapshot et des données en temps réel dans les timers, les effets React, les écouteurs et le code asynchrone.

3701 mots

Un bouton agit sur des données provenant d’une rendu précédent. Un chronomètre enregistre une valeur qui n’existait pas au moment de sa programmation. Chaque gestionnaire créé dans une boucle semble appartenir au dernier élément. Une réponse lente recouvre l’écran de résultats relatifs à une page que l’utilisateur a déjà quittée. Ces bugs sont généralement classés sous la catégorie des « problèmes de fermeture », mais cette étiquette aide rarement quiconque à les résoudre. Cet article remplace cette étiquette par un modèle précis : une fermeture conserve l’accès aux liaisons de variables, et non des copies de valeurs ; chaque tâche différée nécessite une décision explicite quant au fait qu’elle doit voir un instantané ou la valeur en temps réel.

La règle derrière chaque bug de fermeture

Les fermetures ne sont pas imprévisibles. Une fonction JavaScript conserve une référence à l’environnement lexical dans lequel elle a été créée, et chaque fois que son corps fait référence à une variable externe, ce nom est recherché dans l’environnement au moment où le code s’exécute. Le mot clé est l’accès. La fonction ne reçoit pas une copie figée de tout ce qui était visible au moment de sa définition ; elle conserve les mêmes liaisons, et si l’une de ces liaisons est réaffectée par la suite, la fonction lira la nouvelle valeur.

Il existe deux manières opposées de commettre cette erreur. La première consiste à s’attendre à un instantané alors que le code a en réalité créé un lien actif vers une variable qui change. La seconde, fréquente dans les frameworks de rendu, consiste à s’attendre à la valeur la plus récente alors que la fonction de rappel a été créée dans un contexte plus ancien dont les liens ne changeront jamais. Ces deux erreurs proviennent de la même omission : personne n’a décidé auquel moment le travail différé devrait avoir lieu.

Dès que cette décision est explicite, le comportement cesse d’être mystérieux. La fonction de rappel fait précisément ce que le programme lui a demandé de faire. Le programme a simplement exprimé quelque chose de différent de ce que le développeur avait en tête.

Accès, et non photographie

L’exemple de fermeture du manuel scolaire présente une fonction interne qui continue d’utiliser une variable externe après le retour de la fonction externe. Il montre que l’environnement persiste au-delà de l’appel, mais il encourage discrètement une intuition erronée : à savoir que la fonction interne stocke la valeur qu’elle a vue au moment de sa création. Une petite variation met en évidence cette différence. Ici, le générateur d’entrées est créé alors que message vaut "Starting", et la variable est réaffectée avant que le générateur ne retourne.

function createLogger() {
  let message = "Starting";

  const logMessage = () => {
    console.log(message);
  };
  message = "Finished";
  return logMessage;
}
const logger = createLogger();
logger();

L’appel à logger() affiche Finished. La fonction flèche n’a jamais conservé la chaîne "Starting" ; elle a conservé le lien vers message, et au moment où elle s’est exécutée, ce lien pointait vers une autre chaîne.

C’est une fonctionnalité, pas un défaut. L’état mutable privé dans les compteurs, les fonctions de fabrication, les caches de mémorisation et le pattern des modules dépendent tous du fait qu’une clôture puisse voir les mises à jour de ses propres variables. Les problèmes ne commencent qu’à partir du moment où quelqu’un pense que la fonction de rappel est liée à la valeur antérieure plutôt qu’à la variable elle-même.

Les primitives facilitent cette méprise, car les chaînes de caractères et les nombres semblent autonomes, et il semble naturel qu’une fonction conserve simplement « la chaîne de caractères ». Mais le corps d’une fonction contient un nom, pas une valeur, et les noms sont résolus au moment où le code s’exécute. Si vous souhaitez en savoir plus sur la manière dont cette recherche se fait à travers des scopes imbriqués, cet article explique comment la chaîne de scopes de JavaScript résout réellement les variables.

L’habitude pratique à adopter consiste à se poser une seule question chaque fois qu’un callback sera exécuté ultérieurement et fera référence à quelque chose en dehors de lui : doit-il utiliser la valeur telle qu’elle était au moment de la création du callback, ou telle qu’elle est au moment de son exécution ? La réponse indique s’il faut capturer un instantané, lire une référence actuelle, ou transmettre la valeur en tant qu’argument. Sans cette question, le code reste du JavaScript correct, mais son comportement est aléatoire.

Le bug de boucle concerne le lien d’identité

Le bug de fermeture le plus connu concerne une boucle for déclarée avec var qui planifie plusieurs timeouts. Chaque callback semble être lié à son propre itération.

for (var index = 0; index < 3; index++) {
  setTimeout(() => {
    console.log(index);
  }, 100);
}

Ce code affiche 3 trois fois. Une déclaration var a une portée de fonction, ce qui signifie que tout le cycle partage exactement une seule liaison index. Les trois fonctions de rappel font référence à cette même liaison, et au moment où les compteurs déclenchent, le cycle est terminé et la valeur reste à 3.

En utilisant let pour la déclaration, le résultat change, car le langage crée une nouvelle liaison à chaque itération d’un cycle for avec un en-tête let et y copie la valeur actuelle.

for (let index = 0; index < 3; index++) {
  setTimeout(() => {
    console.log(index);
  }, 100);
}

Cette version affiche 0, 1 et 2, car chaque fonction de rappel fait désormais référence à une index différente.

Le résumé habituel est « let corrige les closures », mais cela cache la véritable leçon. Les closures se sont comportés de manière identique dans les deux boucles. Dans la première, trois fonctions de rappel ont été délibérément dotées d’une variable partagée et modifiable ; dans la seconde, chacune en a reçu une propre. Ce qui a changé, c’est l’identité de la liaison, et non le comportement des closures.

Cette distinction est importante car le même bug persiste en l’absence de var. En déclarant une variable mutable avec let en dehors de la boucle, en la mettant à jour à chaque itération et en la lisant dans les fonctions de rappel, chacune verra à nouveau uniquement la valeur finale. Changer de mot-clé ne sert à rien si le code fait toujours pointer plusieurs fonctions de rappel vers une même source mutable.

La meilleure pratique consiste à se demander si les callbacks doivent partager un état. S’ils doivent tous observer une valeur en évolution, une seule liaison est appropriée. Si chacun doit conserver des données spécifiques à son itération, il a besoin de sa propre liaison ou d’un argument explicite. Le fait de voir plusieurs callbacks dans le code pousse à penser que chacun possède les variables qu’il nomme ; le champ lexical indique simplement où les noms sont recherchés, mais non qui en est le propriétaire.

Lorsque le temps sépare la création de l’exécution

Les surprises liées aux closures s’aggravent lorsqu’il y a un délai entre la création d’une fonction et son exécution : un chronomètre, une transmission réseau, une action de l’utilisateur, une tâche en attente dans une file d’attente. Tout ce que la fonction lit à l’extérieur peut changer pendant ce délai. Prenons par exemple une routine de sauvegarde qui dépend d’un identifiant de projet au niveau du module.

let activeProjectId = 42;

async function saveChanges(changes) {
  await saveProject(activeProjectId, changes);
}

activeProjectId = 84;

Cela dépend du moment où cela se produit. Les arguments sont évalués lorsqu’une fonction est appelée, donc si saveChanges s’exécute avant la réaffectation, activeProjectId est lu de manière synchrone et le nombre 42 est ce qui est transmis à saveProject, même si cette dernière fonction attend ensuite. Mais tout callback qui lit activeProjectId après un certain délai verra la valeur que la variable contient à ce moment-là, qui peut être 84, et enregistrera les données dans un projet complètement différent.

C’est pourquoi l’expression « les closures capturent des valeurs » est une abréviation dangereuse. Dans certains codes, une valeur est copiée dans un argument avant la pause ; dans d’autres codes, une référence partagée est lue après celle-ci. Deux fonctions peuvent sembler presque identiques tout en suivant des règles différentes concernant le temps.

Les interfaces utilisateur sont pleines de ce type de schéma. Imaginez une boîte de dialogue de confirmation ouverte pour un enregistrement donné. L’utilisateur se rend sur un autre enregistrement, puis clique sur « Confirmer ». Si le gestionnaire lit la variable « enregistrement actuel », il supprime l’enregistrement affiché à l’écran plutôt que celui pour lequel la boîte de dialogue a été ouverte. La fermeture fonctionne parfaitement ; le produit est incorrect, car l’action aurait dû conserver le contexte initial.

La solution n’est pas automatiquement de « copier la variable ». Il faut d’abord déterminer auquel moment l’opération appartient :

  • Une action destructrice appartient généralement au moment où elle a été initiée et doit utiliser l’ID capturé à ce moment-là.
  • Un indicateur d’état en temps réel a besoin de la valeur la plus récente à chaque mise à jour.
  • Une tentative de réexécution programmée pour plus tard peut combiner les deux : l’ID de l’opération originale, ainsi que le jeton d’autorisation valide au moment où elle est déclenchée.

Les closures obligent les développeurs JavaScript à réfléchir explicitement au temps. La question importante n’est pas seulement quelle variable lit la fonction de rappel, mais quelle version de celle-ci le flux de travail entend utiliser.

Les objets maintiennent une référence stable malgré les changements de contenu

Lorsque le point d’ancrage capturé fait référence à un objet, une nouvelle source de confusion apparaît. Rendre l’ancrage const ne fige rien d’autre que l’ancrage lui-même. Si l’objet est mutable, une fonction de rappel exécutée ultérieurement peut toujours observer tous les changements effectués via la référence partagée.

const settings = {
  retries: 2,
};

setTimeout(() => {
  console.log(settings.retries);
}, 100);

settings.retries = 5;

Le chronomètre affiche 5. La constante settings n’a jamais changé l’objet auquel elle fait référence, mais la propriété retries de cet objet a été mise à jour avant l’exécution de la fonction de rappel.

La console du navigateur peut ajouter à la confusion. Vous enregistrez un objet avant de lancer des tâches asynchrones, vous le développez plus tard dans les outils de développement et vous voyez des champs qui ont été modifiés après l’exécution de l’instruction d’enregistrement. Plusieurs consoles affichent une vue en temps réel de l’objet lorsqu’on le développe, plutôt que le statut qu’il avait au moment de l’enregistrement, ce qui donne l’impression que les enregistrements ont évolué dans le temps. Enregistrer JSON.stringify(obj) ou un clone structuré est un moyen rapide d’obtenir une véritable capture d’écran pendant le débogage.

Créer une véritable capture d’état dans du code nécessite plus qu’une simple nouvelle variable, et la profondeur de la copie dépend de la structure de ce que l’on souhaite protéger. Une copie superficielle, réalisée via spread ou Object.assign, isole les propriétés de niveau supérieur mais partage encore les objets et tableaux imbriqués. Une clonage profond isole davantage, mais peut être coûteux, perdre les prototypes et méthodes de classe, ainsi que dupliquer des références qui devaient être partagées.

Souvent, l’option la plus propre est de ne pas cloner du tout, mais plutôt de sélectionner uniquement les petites valeurs immuables nécessaires à l’opération.

const retryLimit = settings.retries;

setTimeout(() => {
  console.log(retryLimit);
}, 100);

Le callback lit désormais une valeur qui ne changera jamais, et ce qui est tout aussi important, le code indique sur quel élément du contexte historique dépend le timer.

Faites preuve de méfiance envers les tâches différées qui manipulent de grands objets modifiables. Les contextes, les conteneurs de configuration, les objets d’état des composants et les caches partagés en sont des exemples courants. Un callback qui accède à l’un d’eux plus tard dépendra silencieusement de chaque mutation survenue entre-temps. Fournir une charge utile restreinte à la tâche différée constitue un contrat bien plus clair.

React montre l’échec inverse

Au sein de React, les bugs liés aux closures indiquent généralement une autre direction. Le callback ne lit pas une valeur trop récente ; il continue de lire une valeur qui est plutôt ancienne.

Chaque rendu d’un composant fonctionnel correspond à une nouvelle appel de fonction avec ses propres liaisons locales pour les props, l’état et les valeurs dérivées. Un callback créé lors d’un rendu conserve les liaisons de ce rendu spécifique. Lorsque l’état change, React appelle à nouveau le composant et crée de nouvelles liaisons, mais tout callback provenant du rendu précédent qui est encore actif continue de faire référence aux anciennes liaisons.

Les symptômes sont familiers :

  • Un interval créé lors d’un rendu enregistre indéfiniment une valeur obsolète.
  • Un écouteur d’événement enregistré une seule fois continue d’utiliser un prop provenant du premier rendu.
  • Un effet disposant d’une liste de dépendances incomplète continue d’appeler une fonction qui fait référence à un état obsolète.

Cela est appelé une fermeture obsolète, mais la fermeture en question n’est pas défectueuse. Elle reste fidèle à son rendu initial, et rien dans le langage ne la met à jour lorsque React effectue un nouveau rendu.

Cela peut sembler contredire les exemples précédents, où les closures observaient sans problème les mises à jour. La différence réside encore une fois dans l’identité des liens. Dans l’exemple du logger, il y avait un lien qui a été réaffecté, et la closure a donc perçu la nouvelle valeur. React ne réaffecte pas les anciens liens ; il crée des environnements entièrement nouveaux à chaque rendu. L’ancien callback reste attaché à l’ancien environnement, dont les valeurs ne changent jamais.

Vu de cette manière, les solutions découlent directement de la fonction prévue pour le callback :

  • Une mise à jour d’état qui dépend de la valeur actuelle peut utiliser la forme fonctionnelle, comme setCount(c => c + 1), afin que React fournisse l’état le plus récent.
  • Une abonnement qui a besoin de la valeur la plus récente peut être recréé lorsque ses dépendances changent, ou bien lire dans un ref maintenu intentionnellement.
  • Un callback censé agir sur les valeurs provenant de la fonction render qui l’a créé peut déjà être correct tel qu’il est écrit.
  • La question reste la même que précédemment : ce comportement différé doit-il utiliser l’état historique ou l’état actuel ? De nombreux bugs React proviennent du fait que cette question est résolue par hasard, en modifiant un tableau de dépendances, plutôt que par une réflexion sur ce que la fonctionnalité est censée faire. Les versions plus récentes de React proposent également useEffectEvent pour lire les valeurs les plus récentes à l’intérieur d’un effet sans devoir le relancer ; la suppression du ref latest-value avec useEffectEvent explique ce pattern.

    Les tableaux de dépendances décrivent la réalité, pas les préférences

    Une méthode courante pour lutter contre les closures obsolètes consiste à ajuster le tableau de dépendances d’un effet jusqu’à ce que son comportement soit correct. Une valeur y est ajoutée parce que le linter signale un problème, elle en est retirée parce que l’effet s’exécute trop souvent, et finalement le tableau devient vide car l’effet « ne doit s’exécuter qu’une seule fois ».

    Cela traite le tableau comme un bouton de régulation de fréquence. En réalité, il s’agit d’une déclaration indiquant quels valeurs de rendu la closure de l’effet lit.

    Si un effet utilise une variable provenant du rendu environnant, cette variable fait partie de sa closure que celle-ci apparaisse ou non dans le tableau. La laisser de côté ne supprime pas la dépendance ; cela garantit simplement que l’effet continuera d’utiliser la version définie par le dernier rendu qui l’a configurée.

    Inclure toutes les dépendances peut entraîner un problème différent : l’effet détruit et recrée alors une abonnement, redémarre un compteur ou renvoie une requête bien plus souvent que prévu. Cela peut facilement être interprété comme le fait que le vérificateur de syntaxe soit trop strict. Le plus souvent, c’est un signe que l’effet remplit plusieurs fonctions ou qu’un objet ou une fonction dont il dépend est recréé à chaque rendu sans raison.

    Les solutions typiques incluent :

    • stabiliser une fonction de rappel afin qu’elle ne change que lorsque ses propres entrées changent
    • séparer un effet en plusieurs effets ayant des responsabilités plus ciblées
    • placer une fonction d’aide à l’intérieur de l’effet afin qu’elle ne soit plus une dépendance externe
    • utiliser une mise à jour d’état fonctionnelle plutôt que de lire directement l’état
    • se demander si la logique a réellement besoin d’être un effet

    L’objectif n’est pas de forcer React à exécuter l’effet à un rythme souhaité. Il s’agit plutôt de fournir à l’effet une clôture dont la durée de vie correspond au comportement pour lequel il est responsable. Lorsqu’un effet a besoin de valeurs fraîches sans avoir à être démantelé à chaque fois que celles-ci changent, donnez-lui un moyen délibéré de les lire, comme une référence ou un événement d’effet. S’il doit se redémarrer lorsque une valeur change, cette valeur doit figurer dans le tableau. Et si l’effet n’a pas réellement besoin d’une valeur, supprimez plutôt la manière de la lire que la dépendance elle-même.

    Les problèmes liés aux dépendances constituent un retour d’information sur la conception. Ce comportement problématique apparaît lorsque le code déclare une durée de vie pour une clôture alors que la fonctionnalité en nécessite une autre.

    Les écouteurs d’événements survivent au contexte qui les a créés

    Les écouteurs créent délibérément un délai entre l’enregistrement et l’exécution. Vous attachez le gestionnaire une seule fois, et il s’exécute chaque fois que l’événement se produit, éventuellement longtemps après que les variables environnantes aient changé.

    Au JavaScript pur, un écouteur qui lit une variable au niveau du module voit sa valeur la plus récente. Dans un framework de composants, un écouteur attaché lors d’un rendu initial conserve les liens de ce rendu. Dans un cas comme dans l’autre, cette relation est facile à négliger, car le gestionnaire ne s’exécute que lorsque l’utilisateur effectue une action ultérieure.

    Le nettoyage ajoute une autre dimension. Si chaque rendu ajoute un nouvel écouteur sans en supprimer le précédent, plusieurs fonctions de clôture finissent par répondre au même événement, chacune conservant une version différente de l’état. Un seul clic peut alors entraîner plusieurs résultats provenant de différentes étapes de l’histoire de l’application. Le résultat visible peut être une mise à jour dupliquée, un ancien valeur qui réapparaît brièvement, ou un gestionnaire d’événements qui s’exécute après que son composant a été démonté. La cause racine dans chaque cas est que la durée de vie de l’écouteur n’a jamais été alignée avec celle de l’état dont il dépend.

    Un code d’écouteur fiable rend la responsabilité explicite :

    • Gardez une référence à la fonction exacte que vous avez enregistrée, car removeEventListener a besoin de cette même référence.
  • Recréer l’écouteur lorsque le comportement dont il dépend change, ou faire en sorte qu’il lit les valeurs actuelles par l’intermédiaire d’un canal dédié tel qu’une référence ou un stockage.
  • Supprimer l’écouteur lorsque son propriétaire, qu’il s’agisse d’un composant, d’un module ou d’une fonctionnalité, disparaît.
  • Rien de tout cela n’est une formalité. Enregistrer un callback crée un lien entre les événements futurs et l’environnement disponible à ce moment-là. Si ce lien doit prendre fin, le code doit le faire.

    Les réponses asynchrones transmettent une intention ancienne vers une nouvelle page

    Les requêtes réseau génèrent certains des bugs liés à un contexte obsolète les plus coûteux en termes de gestion. Une requête est lancée tandis que l’utilisateur consulte une recherche, un projet ou une route donnés. Avant qu’elle ne soit terminée, l’utilisateur se déplace ailleurs. Lorsque la réponse arrive, son callback écrit dans l’état partagé en utilisant le contexte qu’il a capturé au début.

    Parfois, ce contexte historique est tout à fait exact. Une demande formulée pour le projet 42 doit rester associée à ce dernier, même après que le projet actif soit devenu 84. Le danger réside dans le fait de permettre à ce résultat d’actualiser une interface qui est désormais passée au projet 84. Se souvenir de la demande initiale, c’est là que le mécanisme de clôture remplit sa fonction ; supposer qu’une réponse terminée est encore automatiquement souhaitée, c’est là que la logique échoue.

    C’est pourquoi le raisonnement de clôture et la gestion asynchrone vont de pair. Un callback peut conserver des valeurs historiques parfaitement correctes sans pour autant avoir le droit d’actualiser l’élément cible. Les mécanismes de protection courants incluent :

    • un identifiant ou numéro de séquence de la demande, comparé avant l’application d’un résultat
    • un signal AbortController qui annule les opérations lorsque l’utilisateur passe à autre chose
    • une vérification pour s’assurer que la route ou la sélection actuelle correspond toujours à la demande
  • stocker les résultats sous une clé correspondant au ressource à laquelle ils appartiennent, plutôt que dans une seule case « actuelle »
  • Réécrire simplement la fonction de rappel pour lire le projet actif le plus récent peut aggraver les choses : la réponse concernant le projet 42 serait stockée sous celui du projet 84. Lire l’état actuel n’est pas une solution universelle. Gardez l’identité de la demande intacte et vérifiez que la destination souhaite toujours recevoir le résultat avant de l’écrire.

    Gardez deux contextes séparés. Le contexte de l’opération correspond aux données pour lesquelles la demande a été faite. Le contexte de l’interface correspond à ce que l’utilisateur regarde actuellement. L’écran ne doit être mis à jour que lorsque les deux correspondent encore. Sans cette séparation, les fonctions de rappel semblent voyager dans le temps, fournissant des informations valables d’un moment antérieur à une vue qui a déjà évolué.

    Choix entre capture instantanée et accès en temps réel

    La plupart de ces bugs deviennent simples une fois que l’équipe définit la relation souhaitée.

    Un snapshot signifie que le travail différé utilise les données telles qu’elles étaient au moment de sa création. Cela convient aux identifiants de transaction, à l’identifiant du enregistrement sélectionné, aux valeurs des formulaires soumis, au contexte d’audit et aux commandes qui doivent conserver leur intention initiale. On peut l’implémenter en passant des valeurs en tant qu’arguments, en créant des charges utiles immuables ou en copiant uniquement les champs nécessaires à l’opération.

    Accès en temps réel signifie que le travail différé utilise la valeur la plus récente au moment de son exécution. Cela convient à l’état de la connexion, à la configuration la plus récente, à certains états de l’interface utilisateur dans les gestionnaires d’événements et aux valeurs de coordination modifiables. On peut l’implémenter via un lien partagé, une référence, un accesseur de stockage ou toute autre source explicitement qualifiée de « actuelle ».

    Aucun n’est généralement plus sûr. Des erreurs apparaissent lorsque le code implémente l’un tandis que le développeur suppose l’autre.

    Deux habitudes rendent ce choix visible dans le code. Premièrement, évitez les closures qui accèdent de manière aléatoire à tout ce qui est dans leur portée ; un closure trop large cache ses dépendances, de sorte que le lecteur ne peut pas savoir lesquelles doivent rester figées et lesquelles doivent rester à jour. Deuxièmement, préférez des fonctions petites et à charge restreinte, ce qui réduit le nombre de variables dont la sémantique temporelle doit être prise en compte. Les deux améliorent la fiabilité asynchrone pour la même raison : moins de relations implicites avec le temps.

    Une liste de contrôle rapide pour tout callback exécuté ultérieurement :

    • Quelles variables externes lit-il ?
    • Pour chacune d’entre elles, doit-il voir sa valeur au moment de la création ou lors de l’exécution ?
    • Y a-t-il parmi elles un objet mutable dont le contenu pourrait changer entre-temps ?
  • Qui est propriétaire de ce callback, et quand devrait-il cesser de s’exécuter ?
  • S’il enregistre des résultats quelque part, cet endroit fait-il toujours partie du même contexte ?
  • Conclusion

    Les closures en JavaScript sont déterministes. Elles respectent le champ lexical, conservent les liens associés et maintiennent les environnements actifs tant qu’une fonction accessible en a besoin. Ce qui donne l’impression qu’elles sont hantées, c’est de traiter une variable comme si elle possédait une seule valeur significative dans le temps.

    Chacun des scénarios mentionnés est une variation d’une même question : à quel moment ce callback doit-il appartenir ? Une boucle peut attribuer à tous ses callbacks un même lien partagé. Un chronomètre peut lire un objet après qu’il ait été modifié. Un callback React peut rester lié à une rendu antérieur. Un écouteur peut survivre au état pour lequel il a été créé. Une réponse asynchrone peut transmettre l’intention initiale dans une vue qui a évolué.

    Les développeurs expérimentés rencontrent encore ces bugs parce que les applications modernes diffèrent constamment l’exécution des tâches. Les délais d’expiration, les chaînes de promesses, les événements DOM, les abonnements, les réaffichages, les files d’attente de tâches et les réponses HTTP créent tous un écart entre la définition d’une fonction et son exécution, et plus cet écart est grand, plus le monde qui l’entoure change. La solution n’est pas de mémoriser une autre définition des closures. Il s’agit plutôt de rendre explicites le moment et l’auteur de l’exécution : choisir délibérément entre un accès en snapshot ou en temps réel, ne conserver que les valeurs historiques nécessaires à la tâche, attribuer un seul source intentionnelle à l’état actuel, lier la durée de vie de chaque callback à ce qui en est responsable, et empêcher les tâches obsolètes d’écrire dans des endroits qu’elles ne contrôlent plus. Lorsque ces choix sont visibles dans le code, les closures cessent de vous surprendre, car ils sont enfin liés au moment que vous aviez prévu.