Détecter les changements dans le DOM

242

Je veux exécuter une fonction lorsque des div ou des entrées sont ajoutés au html. Est-ce possible?

Par exemple, une entrée de texte est ajoutée, puis la fonction doit être appelée.

esafwan
la source
1
À moins qu'un script tiers ajoute les nœuds au DOM, cela n'est pas nécessaire.
Justin Johnson
duplicate of stackoverflow.com/questions/2457043/…
Gabriele Petrioli
Duplication possible de Y a
Makyen
4
@JustinJohnson si vous faites une extension chrome qui injecte du code JS, c'est utile.
FluorescentGreen5

Réponses:

207

Mise à jour 2015, nouvelle MutationObserverprise en charge par les navigateurs modernes:

Chrome 18+, Firefox 14+, IE 11+, Safari 6+

Si vous avez besoin de prendre en charge les plus anciennes, vous pouvez essayer de revenir à d'autres approches comme celles mentionnées dans cette réponse de 5 (!) Ans ci-dessous. Il y a des dragons. Prendre plaisir :)


Quelqu'un d'autre change le document? Parce que si vous avez un contrôle total sur les changements, il vous suffit de créer votre propre domChangedAPI - avec une fonction ou un événement personnalisé - et de la déclencher / appeler partout où vous modifiez les choses.

Le DOM niveau 2 a des types d'événements de mutation , mais les anciennes versions d'IE ne le prennent pas en charge. Notez que les événements de mutation sont déconseillés dans la spécification des événements DOM3 et ont une pénalité de performance .

Vous pouvez essayer d'émuler un événement de mutation avec onpropertychangedans IE (et revenir à l'approche par force brute si aucun d'entre eux n'est disponible).

Pour un domChange complet, un intervalle peut être un sur-kill. Imaginez que vous devez stocker l'état actuel de l'ensemble du document et examiner que chaque propriété de chaque élément est identique.

Peut-être que si vous êtes uniquement intéressé par les éléments et leur ordre (comme vous l'avez mentionné dans votre question), un getElementsByTagName("*") peut fonctionner. Cela se déclenchera automatiquement si vous ajoutez un élément, supprimez un élément, remplacez des éléments ou modifiez la structure du document.

J'ai écrit une preuve de concept:

(function (window) {
    var last = +new Date();
    var delay = 100; // default delay

    // Manage event queue
    var stack = [];

    function callback() {
        var now = +new Date();
        if (now - last > delay) {
            for (var i = 0; i < stack.length; i++) {
                stack[i]();
            }
            last = now;
        }
    }

    // Public interface
    var onDomChange = function (fn, newdelay) {
        if (newdelay) delay = newdelay;
        stack.push(fn);
    };

    // Naive approach for compatibility
    function naive() {

        var last = document.getElementsByTagName('*');
        var lastlen = last.length;
        var timer = setTimeout(function check() {

            // get current state of the document
            var current = document.getElementsByTagName('*');
            var len = current.length;

            // if the length is different
            // it's fairly obvious
            if (len != lastlen) {
                // just make sure the loop finishes early
                last = [];
            }

            // go check every element in order
            for (var i = 0; i < len; i++) {
                if (current[i] !== last[i]) {
                    callback();
                    last = current;
                    lastlen = len;
                    break;
                }
            }

            // over, and over, and over again
            setTimeout(check, delay);

        }, delay);
    }

    //
    //  Check for mutation events support
    //

    var support = {};

    var el = document.documentElement;
    var remain = 3;

    // callback for the tests
    function decide() {
        if (support.DOMNodeInserted) {
            window.addEventListener("DOMContentLoaded", function () {
                if (support.DOMSubtreeModified) { // for FF 3+, Chrome
                    el.addEventListener('DOMSubtreeModified', callback, false);
                } else { // for FF 2, Safari, Opera 9.6+
                    el.addEventListener('DOMNodeInserted', callback, false);
                    el.addEventListener('DOMNodeRemoved', callback, false);
                }
            }, false);
        } else if (document.onpropertychange) { // for IE 5.5+
            document.onpropertychange = callback;
        } else { // fallback
            naive();
        }
    }

    // checks a particular event
    function test(event) {
        el.addEventListener(event, function fn() {
            support[event] = true;
            el.removeEventListener(event, fn, false);
            if (--remain === 0) decide();
        }, false);
    }

    // attach test events
    if (window.addEventListener) {
        test('DOMSubtreeModified');
        test('DOMNodeInserted');
        test('DOMNodeRemoved');
    } else {
        decide();
    }

    // do the dummy test
    var dummy = document.createElement("div");
    el.appendChild(dummy);
    el.removeChild(dummy);

    // expose
    window.onDomChange = onDomChange;
})(window);

Usage:

onDomChange(function(){ 
    alert("The Times They Are a-Changin'");
});

Cela fonctionne sur IE 5.5+, FF 2+, Chrome, Safari 3+ et Opera 9.6+

galambalazs
la source
4
Vous vous demandez: comment jQuery live () résout-il ce problème s'il ne peut pas détecter un changement DOM?
Kees C. Bakker
163
Je ne peux pas croire qu'en 2010 vous aviez IE5.5 pour tester cela.
Oscar Godson
1
@JoshStodola L'audacieux m'ennuyait aussi. J'ai décidé de le réparer.
Bojangles
1
Les événements de mutations sont obsolètes. Vous devez utiliser MutationObserver. J'ai écrit mon plugin pour des problèmes comme celui-ci - github.com/AdamPietrasiak/jquery.initialize
pie6k
1
Comment puis-je faire jquery onClick se déclencher devant un observateur de mutation, qui se déclenche lorsqu'un bouton est cliqué avec une action de braise? stackoverflow.com/questions/29216434/…
SuperUberDuper
219

C'est l'approche ultime jusqu'à présent, avec le plus petit code:

IE9 +, FF, Webkit:

Utiliser MutationObserver et revenir aux événements de Mutation obsolètes si nécessaire:
(Exemple ci-dessous si seulement pour les changements DOM concernant les nœuds ajoutés ou supprimés)

var observeDOM = (function(){
  var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

  return function( obj, callback ){
    if( !obj || !obj.nodeType === 1 ) return; // validation

    if( MutationObserver ){
      // define a new observer
      var obs = new MutationObserver(function(mutations, observer){
          callback(mutations);
      })
      // have the observer observe foo for changes in children
      obs.observe( obj, { childList:true, subtree:true });
    }
    
    else if( window.addEventListener ){
      obj.addEventListener('DOMNodeInserted', callback, false);
      obj.addEventListener('DOMNodeRemoved', callback, false);
    }
  }
})();

//------------< DEMO BELOW >----------------
// add item
var itemHTML = "<li><button>list item (click to delete)</button></li>",
    listElm = document.querySelector('ol');

document.querySelector('body > button').onclick = function(e){
  listElm.insertAdjacentHTML("beforeend", itemHTML);
}

// delete item
listElm.onclick = function(e){
  if( e.target.nodeName == "BUTTON" )
    e.target.parentNode.parentNode.removeChild(e.target.parentNode);
}
    
// Observe a specific DOM element:
observeDOM( listElm, function(m){ 
   var addedNodes = [], removedNodes = [];

   m.forEach(record => record.addedNodes.length & addedNodes.push(...record.addedNodes))
   
   m.forEach(record => record.removedNodes.length & removedNodes.push(...record.removedNodes))

  console.clear();
  console.log('Added:', addedNodes, 'Removed:', removedNodes);
});


// Insert 3 DOM nodes at once after 3 seconds
setTimeout(function(){
   listElm.removeChild(listElm.lastElementChild);
   listElm.insertAdjacentHTML("beforeend", Array(4).join(itemHTML));
}, 3000);
<button>Add Item</button>
<ol>
  <li><button>list item (click to delete)</button></li>
  <li><button>list item (click to delete)</button></li>
  <li><button>list item (click to delete)</button></li>
  <li><button>list item (click to delete)</button></li>
  <li><em>&hellip;More will be added after 3 seconds&hellip;</em></li>
</ol>

vsync
la source
1
il semble fonctionner assez bien pour les nouveaux nœuds DOM. Pouvons-nous l'adapter pour gérer également les changements de nœuds dom (au moins les valeurs / texte des nœuds DOM?)
Sebastien Lorber
8
@SebastienLorber - qui est "nous"? vous, en tant que programmeur, pouvez prendre ce code et l'utiliser comme vous le souhaitez. lisez simplement sur le MDN pour quelles choses vous pouvez observer le DOM et pour lesquelles vous ne pouvez pas.
vsync
2
Passez les mutations, observerparamètres à la fonction de rappel pour plus de contrôle.
A1rPun
2
Cela m'a beaucoup aidé, mais comment «dissocier» cela? Supposons que je veuille surveiller un changement une seule fois, mais le faire à plusieurs reprises? oberserveDOM = null ne fonctionnera évidemment pas ...
stiller_leser
Pourquoi cela ne fonctionnerait-il que pour les ajouts / suppressions? Il ressemble à la couverture mutation des événements plus que .. developer.mozilla.org/en-US/docs/Web/Guide/Events/...
f0ster
15

J'ai récemment écrit un plugin qui fait exactement cela - jquery.initialize

Vous l'utilisez de la même manière que la .eachfonction

$(".some-element").initialize( function(){
    $(this).css("color", "blue"); 
});

La différence .eachest - il faut votre sélecteur, dans ce cas.some-element et attendez de nouveaux éléments avec ce sélecteur à l'avenir, si un tel élément est ajouté, il sera également initialisé.

Dans notre cas, la fonction d'initialisation change simplement la couleur de l'élément en bleu. Donc, si nous ajoutons un nouvel élément (que ce soit avec ajax ou même l'inspecteur F12 ou quoi que ce soit) comme:

$("<div/>").addClass('some-element').appendTo("body"); //new element will have blue color!

Le plugin l'initiera instantanément. Le plugin s'assure également qu'un élément n'est initialisé qu'une seule fois. Donc, si vous ajoutez un élément, puis .detach()à partir du corps, puis ajoutez-le à nouveau, il ne sera pas réinitialisé à nouveau.

$("<div/>").addClass('some-element').appendTo("body").detach()
    .appendTo(".some-container");
//initialized only once

Le plugin est basé sur MutationObserver- il fonctionnera sur IE9 et 10 avec les dépendances comme détaillé sur la page readme .

pie6k
la source
Veuillez ajouter à npm.
thexpand
14

ou vous pouvez simplement créer votre propre événement , qui se déroule partout

 $("body").on("domChanged", function () {
                //dom is changed 
            });


 $(".button").click(function () {

          //do some change
          $("button").append("<span>i am the new change</span>");

          //fire event
          $("body").trigger("domChanged");

        });

Exemple complet http://jsfiddle.net/hbmaam/Mq7NX/

HB MAAM
la source
ce n'est pas la même chose ... la méthode décrite ci-dessus est toujours valable api.jquery.com/trigger
lefoy
11

Ceci est un exemple utilisant MutationObserver de Mozilla adapté de ce billet de blog

Chrome 18+, Firefox 14+, IE 11+, Safari 6+

// Select the node that will be observed for mutations
var targetNode = document.getElementById('some-id');

// Options for the observer (which mutations to observe)
var config = { attributes: true, childList: true };

// Callback function to execute when mutations are observed
var callback = function(mutationsList) {
    for(var mutation of mutationsList) {
        if (mutation.type == 'childList') {
            console.log('A child node has been added or removed.');
        }
        else if (mutation.type == 'attributes') {
            console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};

// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);

// Start observing the target node for configured mutations
observer.observe(targetNode, config);

// Later, you can stop observing
observer.disconnect();
Anthony Awuley
la source
4

Utilisez l' interface MutationObserver comme indiqué dans le blog de Gabriele Romanato

Chrome 18+, Firefox 14+, IE 11+, Safari 6+

// The node to be monitored
var target = $( "#content" )[0];

// Create an observer instance
var observer = new MutationObserver(function( mutations ) {
  mutations.forEach(function( mutation ) {
    var newNodes = mutation.addedNodes; // DOM NodeList
    if( newNodes !== null ) { // If there are new nodes added
        var $nodes = $( newNodes ); // jQuery set
        $nodes.each(function() {
            var $node = $( this );
            if( $node.hasClass( "message" ) ) {
                // do something
            }
        });
    }
  });    
});

// Configuration of the observer:
var config = { 
    attributes: true, 
    childList: true, 
    characterData: true 
};

// Pass in the target node, as well as the observer options
observer.observe(target, config);

// Later, you can stop observing
observer.disconnect();
Anthony Awuley
la source
3
MutationObserver est JavaScript natif, pas jQuery.
Benjamin
2

Que diriez-vous d'étendre un jquery pour cela?

   (function () {
        var ev = new $.Event('remove'),
            orig = $.fn.remove;
        var evap = new $.Event('append'),
           origap = $.fn.append;
        $.fn.remove = function () {
            $(this).trigger(ev);
            return orig.apply(this, arguments);
        }
        $.fn.append = function () {
            $(this).trigger(evap);
            return origap.apply(this, arguments);
        }
    })();
    $(document).on('append', function (e) { /*write your logic here*/ });
    $(document).on('remove', function (e) { /*write your logic here*/ ) });

Jquery 1.9+ a construit un support pour cela (j'ai entendu dire non testé).

StartCoding
la source