🌐 AIæœçŽą & 代理 䞻饔
13 octobre 2020

Mutation observer

MutationObserver est un objet intĂ©grĂ© qui observe un Ă©lĂ©ment DOM et dĂ©clenche une callback (fonction de rappel) lorsqu’il dĂ©tecte un changement.

Nous examinerons d’abord la syntaxe, puis nous Ă©tudierons un cas d’utilisation rĂ©el, pour voir oĂč ce genre de chose peut ĂȘtre utile.

Syntaxe

MutationObserver est facile Ă  utiliser.

Tout d’abord, nous crĂ©ons un observateur avec un callback:

let observer = new MutationObserver(callback);

Et ensuite on l’attache à un nƓud DOM:

observer.observe(node, config);

config est un objet avec des options boolĂ©ennes “sur quel type de changements rĂ©agir”:

  • childList – les changements dans les enfants directs de node,
  • subtree – dans tous les descendants de node,
  • attributes – dans les attributs de node,
  • attributeFilter – dans un tableau de noms d’attributs, pour n’observer que ceux qui sont sĂ©lectionnĂ©s,
  • characterData – s’il faut observer node.data (contenu du texte),

Quelques autres options:

  • attributeOldValue – si true, passer l’ancienne et la nouvelle valeur de l’attribut au callback (voir ci-dessous), sinon, seule la nouvelle valeur (a besoin de l’option attributes).
  • characterDataOldValue – si true, passer l’ancienne et la nouvelle valeur de node.data au callback (voir ci-dessous), sinon, seule la nouvelle valeur (a besoin de l’option characterData)

Ensuite, aprĂšs tout changement, le callback est exĂ©cutĂ© : les changements sont passĂ©s dans le premier argument comme une liste d’objets MutationRecord, et l’observer lui-mĂȘme comme deuxiĂšme argument.

Les objects MutationRecord ont les propriétés suivantes:

  • type – type de mutation, valeurs possibles:
    • "attributes": attribut modifiĂ©,
    • "characterData": donnĂ©es modifiĂ©es, utilisĂ©es pour les nƓuds de texte,
    • "childList": Ă©lĂ©ments enfants ajoutĂ©s/supprimĂ©s,
  • target – oĂč le changement a eu lieu: un Ă©lĂ©ment pour les attributes, ou un nƓud de texte pour les characterData, ou un Ă©lĂ©ment pour une mutation childList,
  • addedNodes/removedNodes – les nƓuds qui ont Ă©tĂ© ajoutĂ©s/supprimĂ©s,
  • previousSibling/nextSibling – le frĂšre ou la sƓur prĂ©cĂ©dent(e) et suivant(e) aux nƓuds ajoutĂ©s/supprimĂ©s,
  • attributeName/attributeNamespace – le nom/espace de nommage (pour XML) de l’attribut modifiĂ©,
  • oldValue – la valeur prĂ©cĂ©dente, uniquement pour les modifications d’attributs ou de texte, si l’option correspondante est dĂ©finie attributeOldValue/characterDataOldValue.

Par exemple, voici un <div> avec un attribut contentEditable. Cet attribut nous permet de “focus” contenu et de l’éditer.

<div contentEditable id="elem">Click and <b>edit</b>, please</div>

<script>
let observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // console.log(les changements)
});

// observer tout sauf les attributs
observer.observe(elem, {
  childList: true, // observer les enfants directs
  subtree: true, // et les descendants aussi
  characterDataOldValue: true // transmettre les anciennes données au callback
});
</script>

Si nous exĂ©cutons ce code dans le navigateur, puis qu’on focus la <div> donnĂ© et changeons le texte Ă  l’intĂ©rieur de <b>edit</b>, console.log affichera une mutation:

mutationRecords = [{
  type: "characterData",
  oldValue: "edit",
  target: <text node>,
  // autres propriétés vides
}];

Si nous effectuons des opĂ©rations d’édition plus complexes, par exemple en supprimant le <b>edit</b>, l’évĂ©nement de mutation peut contenir plusieurs enregistrements de mutation:

mutationRecords = [{
  type: "childList",
  target: <div#elem>,
  removedNodes: [<b>],
  nextSibling: <text node>,
  previousSibling: <text node>
  // autres propriétés vides
}, {
  type: "characterData"
  target: <text node>
  // ...les détails de la mutation dépendent de la façon dont le navigateur gÚre cette suppression
  // il peut regrouper deux nƓuds de texte adjacents "edit" et ", please" en un seul nƓud
  // ou il peut leur laisser des nƓuds de texte sĂ©parĂ©s
}];

MutationObserver permet donc de réagir à tout changement dans le sous-arbre DOM

Utilisation pour l’intĂ©gration

Quand une telle chose peut-elle ĂȘtre utile ?

Imaginez la situation oĂč vous devez ajouter un script tiers qui contient des fonctionnalitĂ©s utiles, mais qui fait aussi quelque chose d’indĂ©sirable, par exemple afficher des annonces <div class="ads">Unwanted ads</div>.

Naturellement, le script tiers ne prévoit aucun mécanisme permettant de le supprimer.

GrĂące Ă  MutationObserver, nous pouvons dĂ©tecter quand l’élĂ©ment indĂ©sirable apparaĂźt dans notre DOM et le supprimer.

Il y a d’autres situations oĂč un script tiers ajoute quelque chose dans notre document, et nous aimerions dĂ©tecter, quand cela se produit, d’adapter notre page, de redimensionner dynamiquement quelque chose, etc.

MutationObserver permet de faire tout ça.

Utilisation pour l’architecture

Il y a aussi des situations oĂč MutationObserver est bon du point de vue architectural.

Disons que nous faisons un site web sur la programmation. Naturellement, les articles et autres matériels peuvent contenir des extraits de code source.

Voici Ă  quoi ressemble un tel extrait dans un balisage HTML:

...
<pre class="language-javascript"><code>
  // voici le code
  let hello = "world";
</code></pre>
...

Pour une meilleure lisibilitĂ© et en mĂȘme temps, pour l’embellir, nous utiliserons une bibliothĂšque de coloration syntaxique JavaScript sur notre site, comme Prism.js. Pour obtenir la coloration syntaxique de l’extrait de code ci-dessus dans Prism, Prism.highlightElem(pre) est appelĂ©, qui examine le contenu de ces Ă©lĂ©ments pre et ajoute des balises et des styles spĂ©ciaux pour la coloration syntaxique colorĂ©e dans ces Ă©lĂ©ments, similaire Ă  ce que vous voyez en exemples ici, sur cette page.

Quand exactement faut-il appliquer cette mĂ©thode de mise en Ă©vidence ? Nous pouvons le faire sur l’évĂ©nement DOMContentLoaded, ou en bas de page. À ce moment, nous avons notre DOM prĂȘt, nous pouvons rechercher des Ă©lĂ©ments pre[class*="language"] et appeler Prism.highlightElem dessus :

// mettre en évidence tous les extraits de code sur la page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);

Tout est simple jusqu’à prĂ©sent, n’est-ce pas ? Nous trouvons des extraits de code en HTML et les mettons en Ă©vidence.

Maintenant, continuons. Disons que nous allons chercher dynamiquement des Ă©lĂ©ments sur un serveur. Nous Ă©tudierons les mĂ©thodes pour cela plus tard dans le tutoriel. Pour l’instant, il suffit d’aller chercher un article HTML sur un serveur web et de l’afficher Ă  la demande :

let article = /* récupérer du nouveau contenu sur le serveur */
articleElem.innerHTML = article;

Le nouvel article HTML peut contenir des extraits de code. Nous devons appeler Prism.highlightElem sur eux, sinon ils ne seront pas mis en évidence.

OĂč et quand appeler Prism.highlightElem pour un article chargĂ© dynamiquement ?

Nous pourrions ajouter cet appel au code qui charge un article, comme ceci:

let article = /* récupérer du nouveau contenu sur le serveur */
articleElem.innerHTML = article;

let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);


 Mais imaginez si nous avons de nombreux endroits dans le code oĂč nous chargeons notre contenu – articles, quiz, messages de forum, etc. Devons-nous mettre l’appel de mise en Ă©vidence partout, pour mettre en Ă©vidence le code dans le contenu aprĂšs le chargement? Ce n’est pas trĂšs pratique.

Et si le contenu est chargĂ© par un module tiers ? Par exemple, nous avons un forum Ă©crit par quelqu’un d’autre, qui charge le contenu dynamiquement, et nous aimerions y ajouter une mise en Ă©vidence syntaxique. Personne n’aime patcher des scripts tiers.

Heureusement, il y a une autre option.

Nous pouvons utiliser MutationObserver pour détecter automatiquement quand des extraits de code sont insérés dans la page et les mettre en évidence.

Nous allons donc gérer la fonctionnalité de mise en évidence en un seul endroit.

Démonstration dynamique de mise en évidence

Si vous exĂ©cutez ce code, il commence Ă  observer l’élĂ©ment ci-dessous et Ă  mettre en Ă©vidence tout extrait de code qui y apparaĂźt:

let observer = new MutationObserver(mutations => {

  for(let mutation of mutations) {
    // examiner les nouveaux nƓuds, y a-t-il quelque chose Ă  mettre en Ă©vidence ?

    for(let node of mutation.addedNodes) {
      // nous ne suivons que les Ă©lĂ©ments, nous sautons les autres nƓuds (par exemple les nƓuds de texte)
      if (!(node instanceof HTMLElement)) continue;

      // vérifier que l'élément inséré est un extrait de code
      if (node.matches('pre[class*="language-"]')) {
        Prism.highlightElement(node);
      }

      // ou peut-ĂȘtre qu'il y a un extrait de code quelque part dans son sous-arbre ?
      for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
        Prism.highlightElement(elem);
      }
    }
  }

});

let demoElem = document.getElementById('highlight-demo');

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

Ci-dessous, il y a un élément HTML et JavaScript qui le remplit dynamiquement en utilisant innerHTML.

Veuillez exĂ©cuter le code prĂ©cĂ©dent (ci-dessus, qui observe cet Ă©lĂ©ment), puis le code ci-dessous. Vous verrez comment MutationObserver dĂ©tecte et met en Ă©vidence l’extrait.

Voici un élément de démonstration avec id="highlight-demo", exécutez le code ci-dessus pour l'observer.

Le code suivant remplit son innerHTML, qui fait réagir le MutationObserver et met en évidence son contenu:

let demoElem = document.getElementById('highlight-demo');

// insérer dynamiquement du contenu avec des extraits de code
demoElem.innerHTML = `Vous trouverez ci-dessous un extrait de code:
  <pre class="language-javascript"><code> let hello = "world!"; </code></pre>
  <div>Un autre:</div>
  <div>
    <pre class="language-css"><code>.class { margin: 5px; } </code></pre>
  </div>
`;

Nous avons maintenant MutationObserver qui peut suivre tous les surlignages dans les éléments observés ou dans le document entier. Nous pouvons ajouter/supprimer des bribes de code en HTML sans y penser.

Méthodes supplémentaires

Il y a une mĂ©thode pour arrĂȘter d’observer le nƓud:

  • observer.disconnect() – arrĂȘte l’observation.

Lorsque nous arrĂȘtons l’observation, il est possible que certaines modifications n’aient pas encore Ă©tĂ© traitĂ©es par l’observateur.

  • observer.takeRecords() – obtient une liste des dossiers de mutation non traitĂ©s, ceux qui se sont produits, mais le rappel n’a pas permis de les traiter.

Ces mĂ©thodes peuvent ĂȘtre utilisĂ©es ensemble, comme ceci:

// obtenir une liste des mutations non traitées
// doit ĂȘtre appelĂ© avant de se dĂ©connecter,
// si vous vous souciez de mutations récentes éventuellement non gérées
let mutationRecords = observer.takeRecords();

// stop tracking changes
observer.disconnect();
...
Les enregistrements retournĂ©s par Ê»observer.takeRecords() `sont supprimĂ©s de la file d’attente de traitement

Le rappel ne sera pas appelé pour les enregistrements, renvoyé par observer.takeRecords().

Interaction avec le garbage collection

Les observateurs utilisent des rĂ©fĂ©rences faibles aux nƓuds en interne. Autrement dit, si un nƓud est retirĂ© du DOM et devient inaccessible, il devient alors un dĂ©chet collectĂ©.

Le simple fait qu’un nƓud DOM soit observĂ© n’empĂȘche pas le ramassage des ordures.

Résumé

MutationObserver peut rĂ©agir aux changements dans le DOM – attributs, contenu de texte et ajout / suppression d’élĂ©ments.

Nous pouvons l’utiliser pour suivre les changements introduits par d’autres parties de notre code, ainsi que pour intĂ©grer des scripts tiers.

MutationObserver peut suivre tout changement. Les options de configuration “ce qu’il faut observer” sont utilisĂ©es pour des optimisations, afin de ne pas dĂ©penser des ressources pour des callback inutiles.

Carte du tutoriel

Commentaires

lire ceci avant de commenter

  • Si vous avez des amĂ©liorations Ă  suggĂ©rer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de prĂ©ciser.
  • Pour insĂ©rer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen
)