Apprendre à connaître l'API MutationObserver
Publié: 2022-03-10Dans 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é.
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éthodeobserve()
. - 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 surtrue
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'objetmutation
, 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.