</> HTML5Advent
ENFRESDEITPT

// apis

MutationObserver, c'est quoi ? Le troisième observateur, et la file qui le rend utilisable

IntersectionObserver surveille la visibilité, ResizeObserver la taille, MutationObserver le DOM lui-même. Ce qu'il rapporte, pourquoi il a remplacé les Mutation Events, et la méthode qui révèle comment la livraison fonctionne réellement.

Un carnet à spirale ouvert sur des pages quadrillées vierges avec un stylo noir posé dessus, à côté d'un passeport et d'un smartphone sur une table en bois

La plateforme compte trois observateurs, et ils se partagent le travail proprement. IntersectionObserver vous dit quand un élément entre dans la fenêtre d'affichage. ResizeObserver vous dit quand il change de taille. MutationObserver vous dit quand le DOM lui-même change.

MDN le formule en une ligne : l'interface « offre la possibilité de surveiller les changements apportés à l'arbre DOM ».

Ce qu'il peut surveiller

Trois types de changement, choisis par les options que vous passez :

  • childList — des nœuds ajoutés ou retirés
  • attributes — un attribut modifié sur l'élément observé
  • characterData — le contenu textuel d'un nœud modifié

Plus subtree, qui étend n'importe lequel des précédents à tous les descendants plutôt qu'à la seule cible. Ce mot fait toute la différence entre surveiller un conteneur et surveiller tout ce qu'il contient.

const observer = new MutationObserver((records) =&gt; {
  for (const record of records) {
    if (record.type === 'childList') {
      console.log(record.addedNodes.length, 'node(s) added');
    }
  }
});

observer.observe(targetNode, {
  attributes: true,
  childList: true,
  subtree: true,
});

Pourquoi il a remplacé les Mutation Events

MDN est explicite sur la filiation : MutationObserver « est conçu comme un remplacement de l'ancienne fonctionnalité Mutation Events, qui faisait partie de la spécification DOM3 Events ».

Les Mutation Events se déclenchaient de façon synchrone, un événement par changement, au milieu de ce qui modifiait le DOM. Un script insérant cent nœuds produisait cent interruptions, et un gestionnaire qui touchait au DOM pouvait déclencher d'autres événements alors qu'il traitait encore le premier. Ils ont été retirés pour de bonnes raisons.

La file d'attente est la conception

La partie intéressante n'est pas le callback mais ce qui l'alimente, et une méthode le trahit. takeRecords() « retire toutes les notifications en attente de la file de notifications du MutationObserver et les renvoie dans un nouveau tableau d'objets MutationRecord ».

Gros plan d'un bloc-notes en papier sur un bureau, avec un stylo et une paire de lunettes à côté

Une file de notifications en attente signifie que la livraison n'est pas immédiate. Les changements s'accumulent, et votre callback reçoit un tableau d'enregistrements plutôt qu'un appel par changement. C'est précisément ce qui rend l'API utilisable là où les Mutation Events ne l'étaient pas : cent insertions deviennent un callback avec cent enregistrements, et votre code ne peut pas interrompre l'opération qui les a causés.

Cela explique aussi quand takeRecords() gagne sa place. Avant d'appeler disconnect(), tout ce qui est déjà en file mais pas encore livré serait perdu : vider la file d'abord permet de le traiter.

Deux choses qui piègent

Vos propres écritures sont observées aussi. Si le callback modifie le DOM à l'intérieur du sous-arbre observé, ces modifications entrent en file et vous rappellent. Le remède habituel est un drapeau de garde, ou un resserrement des options pour que vos propres écritures tombent hors de ce que vous surveillez.

Les enregistrements d'attribut ne portent pas la nouvelle valeur par défaut. Vous obtenez le nom de l'attribut, et attributeOldValue: true dans les options si vous voulez la précédente. La valeur actuelle, vous la lisez vous-même sur l'élément.

Quand y recourir

MutationObserver est le bon outil quand le DOM est modifié par quelque chose que vous ne contrôlez pas : un widget tiers, un bloc injecté par un CMS, un éditeur. C'est le mauvais outil pour l'état de votre propre application, où vous savez déjà quand les choses changent et où les observer revient à vous poser lentement une question que vous pouvez trancher directement.

Et disconnect() compte plus qu'il n'en a l'air : MDN le décrit comme arrêtant les notifications « jusqu'à ce qu'observe() soit appelé de nouveau, et seulement dans ce cas ». Un observateur posé sur un sous-arbre et qui survit au composant qui le surveillait est une fuite que personne ne remarque avant une heure de page ouverte.

Un bloc-notes en attente d'être lu. takeRecords() fait exactement cela : il vide la file d'attente de l'observateur et vous remet ce qui s'y est accumulé depuis la dernière livraison.