Apprendre à connaître l'API MutationObserver

Publié: 2022-03-10
Résumé rapide ↬ La surveillance des modifications apportées au DOM est parfois nécessaire dans les applications Web et les frameworks complexes. Au moyen d'explications et de démonstrations interactives, cet article vous montrera comment vous pouvez utiliser l'API MutationObserver pour rendre l'observation des changements DOM relativement facile.

Dans les applications Web complexes, les changements DOM peuvent être fréquents. Par conséquent, il peut arriver que votre application doive répondre à une modification spécifique du DOM.

Pendant un certain temps, la méthode acceptée pour rechercher les modifications apportées au DOM consistait à utiliser une fonctionnalité appelée Mutation Events, qui est désormais obsolète. Le remplacement approuvé par le W3C pour Mutation Events est l'API MutationObserver, dont je parlerai en détail dans cet article.

Un certain nombre d'articles et de références plus anciens expliquent pourquoi l'ancienne fonctionnalité a été remplacée, je n'entrerai donc pas dans les détails à ce sujet ici (en plus du fait que je ne serais pas en mesure de lui rendre justice). L'API MutationObserver a une prise en charge presque complète des navigateurs, nous pouvons donc l'utiliser en toute sécurité dans la plupart des projets, sinon tous, en cas de besoin.

Syntaxe de base pour un MutationObserver

Un MutationObserver peut être utilisé de différentes manières, que je couvrirai en détail dans le reste de cet article, mais la syntaxe de base d'un MutationObserver ressemble à ceci :

 let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);

La première ligne crée un nouveau MutationObserver en utilisant le constructeur MutationObserver() . L'argument passé au constructeur est une fonction de rappel qui sera appelée à chaque modification DOM qualifiée.

La façon de déterminer ce qui se qualifie pour un observateur particulier est au moyen de la dernière ligne du code ci-dessus. Sur cette ligne, j'utilise la méthode observe() de MutationObserver pour commencer à observer. Vous pouvez comparer cela à quelque chose comme addEventListener() . Dès que vous attachez un écouteur, la page "écoutera" l'événement spécifié. De même, lorsque vous commencez à observer, la page commencera à "observer" le MutationObserver spécifié.

Plus après saut! Continuez à lire ci-dessous ↓

La méthode observe() prend deux arguments : La cible , qui doit être le nœud ou l'arbre de nœuds sur lequel observer les changements ; et un objet options , qui est un objet MutationObserverInit qui vous permet de définir la configuration de l'observateur.

La dernière fonctionnalité clé de base d'un MutationObserver est la méthode disconnect() . Cela vous permet d'arrêter d'observer les modifications spécifiées, et cela ressemble à ceci :

 observer.disconnect();

Options pour configurer un MutationObserver

Comme mentionné, la méthode observe() d'un MutationObserver nécessite un deuxième argument qui spécifie les options pour décrire le MutationObserver . Voici à quoi ressemblerait l'objet options avec toutes les paires propriété/valeur possibles incluses :

 let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };

Lors de la configuration des options de MutationObserver , il n'est pas nécessaire d'inclure toutes ces lignes. Je les inclue simplement à des fins de référence, afin que vous puissiez voir quelles options sont disponibles et quels types de valeurs elles peuvent prendre. Comme vous pouvez le voir, tous sauf un sont booléens.

Pour qu'un MutationObserver fonctionne, au moins l'un des childList , attributes ou characterData doit être défini sur true , sinon une erreur sera générée. Les quatre autres propriétés fonctionnent en conjonction avec l'une de ces trois (plus à ce sujet plus tard).

Jusqu'à présent, j'ai simplement passé sous silence la syntaxe pour vous donner un aperçu. La meilleure façon d'examiner le fonctionnement de chacune de ces fonctionnalités consiste à fournir des exemples de code et des démonstrations en direct qui intègrent les différentes options. C'est donc ce que je vais faire pour le reste de cet article.

Observation des modifications apportées aux éléments enfants à l'aide de childList

Le premier et le plus simple MutationObserver que vous pouvez lancer est celui qui recherche les nœuds enfants d'un nœud spécifié (généralement un élément) à ajouter ou à supprimer. Pour mon exemple, je vais créer une liste non ordonnée dans mon HTML, et je veux savoir quand un nœud enfant est ajouté ou supprimé de cet élément de liste.

Le code HTML de la liste ressemble à ceci :

 <ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>

Le JavaScript pour mon MutationObserver inclut les éléments suivants :

 let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);

Ce n'est qu'une partie du code. Par souci de brièveté, je montre les sections les plus importantes qui traitent de l'API MutationObserver elle-même.

Remarquez comment je parcours l'argument mutations , qui est un objet MutationRecord qui a un certain nombre de propriétés différentes. Dans ce cas, je lis la propriété type et j'enregistre un message indiquant que le navigateur a détecté une mutation qui se qualifie. Notez également que je passe l'élément mList (une référence à ma liste HTML) comme élément ciblé (c'est-à-dire l'élément sur lequel je souhaite observer les modifications).

  • Voir la démo interactive complète →

Utilisez les boutons pour démarrer et arrêter le MutationObserver . Les messages du journal aident à clarifier ce qui se passe. Les commentaires dans le code fournissent également des explications.

Notez ici quelques points importants :

  • La fonction de rappel (que j'ai nommée mCallback , pour illustrer que vous pouvez la nommer comme vous voulez) se déclenchera chaque fois qu'une mutation réussie est détectée et après l'exécution de la méthode observe() .
  • Dans mon exemple, le seul "type" de mutation qui se qualifie est childList , il est donc logique de rechercher celui-ci lors de la boucle dans MutationRecord. Rechercher un autre type dans cette instance ne ferait rien (les autres types seront utilisés dans les démos suivantes).
  • En utilisant childList , je peux ajouter ou supprimer un nœud de texte de l'élément ciblé et cela aussi serait admissible. Il n'est donc pas nécessaire que ce soit un élément ajouté ou supprimé.
  • Dans cet exemple, seuls les nœuds enfants immédiats seront éligibles. Plus loin dans l'article, je vous montrerai comment cela peut s'appliquer à tous les nœuds enfants, petits-enfants, etc.

Observer les modifications apportées aux attributs d'un élément

Un autre type de mutation courant que vous souhaiterez peut-être suivre est le changement d'un attribut sur un élément spécifié. Dans la prochaine démo interactive, je vais observer les modifications apportées aux attributs d'un élément de paragraphe.

 let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
  • Essayez la démo →

Encore une fois, j'ai abrégé le code pour plus de clarté, mais les parties importantes sont :

  • L'objet options utilise la propriété attributes , définie sur true pour indiquer à MutationObserver que je souhaite rechercher des modifications des attributs de l'élément ciblé.
  • Le type de mutation que je teste dans ma boucle est attributes , le seul qui se qualifie dans ce cas.
  • J'utilise également la propriété attributeName de l'objet mutation , ce qui me permet de savoir quel attribut a été modifié.
  • Lorsque je déclenche l'observateur, je transmets l'élément de paragraphe par référence, ainsi que les options.

Dans cet exemple, un bouton est utilisé pour basculer un nom de classe sur l'élément HTML ciblé. La fonction de rappel dans l'observateur de mutation est déclenchée chaque fois que la classe est ajoutée ou supprimée.

Observation des changements de données de caractère

Un autre changement que vous voudrez peut-être rechercher dans votre application concerne les mutations des données de personnage. c'est-à-dire, des modifications à un nœud de texte spécifique. Cela se fait en définissant la propriété characterData sur true dans l'objet options . Voici le code :

 let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }

Notez à nouveau que le type recherché dans la fonction de rappel est characterData .

  • Voir la démo en direct →

Dans cet exemple, je recherche des modifications sur un nœud de texte spécifique, que je cible via element.childNodes[0] . C'est un peu hacky mais ça ira pour cet exemple. Le texte est modifiable par l'utilisateur via l'attribut contenteditable sur un élément de paragraphe.

Défis lors de l'observation des changements de données de caractère

Si vous avez joué avec contenteditable , vous savez peut-être qu'il existe des raccourcis clavier qui permettent l'édition de texte enrichi. Par exemple, CTRL-B met le texte en gras, CTRL-I met le texte en italique, etc. Cela divisera le nœud de texte en plusieurs nœuds de texte, vous remarquerez donc que MutationObserver cessera de répondre à moins que vous ne modifiiez le texte qui est toujours considéré comme faisant partie du nœud d'origine.

Je dois également souligner que si vous supprimez tout le texte, le MutationObserver ne déclenchera plus le rappel. Je suppose que cela se produit car une fois que le nœud de texte disparaît, l'élément cible n'existe plus. Pour lutter contre cela, ma démo arrête d'observer lorsque le texte est supprimé, bien que les choses deviennent un peu collantes lorsque vous utilisez des raccourcis de texte enrichi.

Mais ne vous inquiétez pas, plus loin dans cet article, je discuterai d'une meilleure façon d'utiliser l'option characterData sans avoir à gérer autant de ces bizarreries.

Observation des modifications apportées aux attributs spécifiés

Plus tôt, je vous ai montré comment observer les modifications apportées aux attributs d'un élément spécifié. Dans ce cas, bien que la démo déclenche un changement de nom de classe, j'aurais pu modifier n'importe quel attribut sur l'élément spécifié. Mais que se passe-t-il si je veux observer des changements sur un ou plusieurs attributs spécifiques tout en ignorant les autres ?

Je peux le faire en utilisant la propriété facultative attributeFilter dans l'objet option . Voici un exemple :

 let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }

Comme indiqué ci-dessus, la propriété attributeFilter accepte un tableau d'attributs spécifiques que je souhaite surveiller. Dans cet exemple, le MutationObserver déclenchera le rappel chaque fois qu'un ou plusieurs des attributs hidden , contenteditable ou data-par sont modifiés.

  • Voir la démo en direct →

Encore une fois, je cible un élément de paragraphe spécifique. Remarquez la liste déroulante de sélection qui choisit l'attribut qui sera modifié. L'attribut draggable est le seul qui ne sera pas admissible car je ne l'ai pas spécifié dans mes options.

Remarquez dans le code que j'utilise à nouveau la propriété attributeName de l'objet MutationRecord pour enregistrer quel attribut a été modifié. Et bien sûr, comme avec les autres démos, le MutationObserver ne commencera pas à surveiller les changements tant que le bouton "démarrer" n'aura pas été cliqué.

Une autre chose que je dois souligner ici est que je n'ai pas besoin de définir la valeur des attributes sur true dans ce cas ; c'est sous-entendu car attributesFilter est défini sur true. C'est pourquoi mon objet options pourrait ressembler à ceci, et cela fonctionnerait de la même manière :

 let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }

D'un autre côté, si je définissais explicitement les attributes sur false avec un tableau attributeFilter , cela ne fonctionnerait pas car la valeur false aurait priorité et l'option de filtre serait ignorée.

Observer les modifications apportées aux nœuds et à leur sous-arborescence

Jusqu'à présent, lors de la configuration de chaque MutationObserver , je n'ai traité que l'élément ciblé lui-même et, dans le cas de childList , les enfants immédiats de l'élément. Mais il pourrait certainement y avoir un cas où je pourrais vouloir observer des changements à l'un des éléments suivants :

  • Un élément et tous ses éléments enfants ;
  • Un ou plusieurs attributs sur un élément et sur ses éléments enfants ;
  • Tous les nœuds de texte à l'intérieur d'un élément.

Tout ce qui précède peut être réalisé en utilisant la propriété subtree de l'objet options.

childList Avec sous-arborescence

Tout d'abord, recherchons les modifications apportées aux nœuds enfants d'un élément, même s'il ne s'agit pas d'enfants immédiats. Je peux modifier mon objet options pour ressembler à ceci:

 options = { childList: true, subtree: true }

Tout le reste du code est plus ou moins identique à l'exemple précédent childList , avec quelques balises et boutons supplémentaires.

  • Voir la démo en direct →

Ici, il y a deux listes, l'une imbriquée dans l'autre. Lorsque le MutationObserver est démarré, le rappel se déclenchera pour les modifications apportées à l'une ou l'autre des listes. Mais si je devais redéfinir la propriété subtree sur false (valeur par défaut lorsqu'elle n'est pas présente), le rappel ne s'exécuterait pas lorsque la liste imbriquée est modifiée.

Attributs avec sous-arborescence

Voici un autre exemple, cette fois en utilisant subtree avec des attributes et attributeFilter . Cela me permet d'observer les modifications apportées aux attributs non seulement sur l'élément cible, mais également sur les attributs de tous les éléments enfants de l'élément cible :

 options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
  • Voir la démo en direct →

Ceci est similaire à la démo des attributs précédents, mais cette fois j'ai mis en place deux éléments de sélection différents. Le premier modifie les attributs de l'élément de paragraphe ciblé tandis que l'autre modifie les attributs d'un élément enfant à l'intérieur du paragraphe.

Encore une fois, si vous deviez remettre l'option subtree à false (ou la supprimer), le deuxième bouton bascule ne déclencherait pas le rappel MutationObserver . Et, bien sûr, je pourrais omettre complètement attributeFilter , et le MutationObserver rechercherait les modifications apportées à tous les attributs du sous-arbre plutôt qu'à ceux spécifiés.

characterData Avec sous-arborescence

Rappelez-vous que dans la démo précédente de characterData , il y avait quelques problèmes avec la disparition du nœud ciblé, puis le MutationObserver ne fonctionnait plus. Bien qu'il existe des moyens de contourner cela, il est plus facile de cibler un élément directement plutôt qu'un nœud de texte, puis d'utiliser la propriété subtree pour spécifier que je veux toutes les données de caractère à l'intérieur de cet élément, quelle que soit leur profondeur, pour déclencher le rappel MutationObserver .

Mes options dans ce cas ressembleraient à ceci:

 options = { characterData: true, subtree: true }
  • Voir la démo en direct →

Après avoir démarré l'observateur, essayez d'utiliser CTRL-B et CTRL-I pour formater le texte modifiable. Vous remarquerez que cela fonctionne beaucoup plus efficacement que l'exemple précédent de characterData . Dans ce cas, les nœuds enfants brisés n'affectent pas l'observateur car nous observons tous les nœuds à l'intérieur du nœud ciblé, au lieu d'un seul nœud de texte.

Enregistrement des anciennes valeurs

Souvent, lors de l'observation des modifications apportées au DOM, vous souhaiterez prendre note des anciennes valeurs et éventuellement les stocker ou les utiliser ailleurs. Cela peut être fait en utilisant quelques propriétés différentes dans l'objet options .

attributOldValue

Tout d'abord, essayons de nous déconnecter de l'ancienne valeur d'attribut après sa modification. Voici à quoi ressembleront mes options avec mon rappel :

 options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
  • Voir la démo en direct →

Notez l'utilisation des propriétés attributeName et oldValue de l'objet MutationRecord . Essayez la démo en saisissant différentes valeurs dans le champ de texte. Remarquez comment le journal est mis à jour pour refléter la valeur précédente qui a été stockée.

characterDataOldValue

De même, voici à quoi ressembleraient mes options si je souhaitais enregistrer d'anciennes données de personnage :

 options = { characterData: true, subtree: true, characterDataOldValue: true }
  • Voir la démo en direct →

Notez que les messages du journal indiquent la valeur précédente. Les choses deviennent un peu bancales lorsque vous ajoutez du HTML via des commandes de texte enrichi au mélange. Je ne sais pas quel est le comportement correct dans ce cas, mais c'est plus simple si la seule chose à l'intérieur de l'élément est un seul nœud de texte.

Intercepter les mutations à l'aide de takeRecords()

Une autre méthode de l'objet MutationObserver que je n'ai pas encore mentionnée est takeRecords() . Cette méthode permet d'intercepter plus ou moins les mutations détectées avant qu'elles ne soient traitées par la fonction de rappel.

Je peux utiliser cette fonctionnalité en utilisant une ligne comme celle-ci :

 let myRecords = observer.takeRecords();

Cela stocke une liste des changements DOM dans la variable spécifiée. Dans ma démo, j'exécute cette commande dès que le bouton qui modifie le DOM est cliqué. Notez que les boutons démarrer et ajouter/supprimer n'enregistrent rien. En effet, comme mentionné, j'intercepte les modifications DOM avant qu'elles ne soient traitées par le rappel.

Remarquez, cependant, ce que je fais dans l'écouteur d'événements qui arrête l'observateur :

 btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);

Comme vous pouvez le voir, après avoir arrêté l'observateur à l'aide de observer.disconnect() , j'accède à l'enregistrement de mutation qui a été intercepté et j'enregistre l'élément cible ainsi que le type de mutation qui a été enregistré. Si j'avais observé plusieurs types de modifications, l'enregistrement stocké contiendrait plus d'un élément, chacun avec son propre type.

Lorsqu'un enregistrement de mutation est intercepté de cette manière en appelant takeRecords() , la file d'attente des mutations qui seraient normalement envoyées à la fonction de rappel est vidée. Donc, si pour une raison quelconque vous avez besoin d'intercepter ces enregistrements avant qu'ils ne soient traités, takeRecords() serait utile.

Observer plusieurs changements à l'aide d'un seul observateur

Notez que si je recherche des mutations sur deux nœuds différents sur la page, je peux le faire en utilisant le même observateur. Cela signifie qu'après avoir appelé le constructeur, je peux exécuter la méthode observe() pour autant d'éléments que je le souhaite.

Ainsi, après cette ligne :

 observer = new MutationObserver(mCallback);

Je peux alors avoir plusieurs appels observe() avec différents éléments comme premier argument :

 observer.observe(mList, options); observer.observe(mList2, options);
  • Voir la démo en direct →

Démarrez l'observateur, puis essayez les boutons ajouter/supprimer pour les deux listes. Le seul hic ici est que si vous appuyez sur l'un des boutons "stop", l'observateur arrêtera d'observer les deux listes, pas seulement celle qu'il cible.

Déplacement d'un arbre de nœuds observé

Une dernière chose que je soulignerai est qu'un MutationObserver continuera à observer les modifications apportées à un nœud spécifié même après que ce nœud a été supprimé de son élément parent.

Par exemple, essayez la démo suivante :

  • Voir la démo en direct →

Ceci est un autre exemple qui utilise childList pour surveiller les modifications apportées aux éléments enfants d'un élément cible. Remarquez le bouton qui déconnecte la sous-liste, qui est celle qui est observée. Cliquez sur le bouton "Démarrer...", puis cliquez sur le bouton "Déplacer..." pour déplacer la liste imbriquée. Même après la suppression de la liste de son parent, le MutationObserver continue d'observer les modifications spécifiées. Ce n'est pas une grande surprise que cela se produise, mais c'est quelque chose à garder à l'esprit.

Conclusion

Cela couvre à peu près toutes les fonctionnalités principales de l'API MutationObserver . J'espère que cette analyse approfondie vous a été utile pour vous familiariser avec cette norme. Comme mentionné, la prise en charge des navigateurs est solide et vous pouvez en savoir plus sur cette API sur les pages de MDN.

J'ai mis toutes les démos de cet article dans une collection CodePen, si vous voulez avoir un endroit facile pour jouer avec les démos.