Detectar cambios en el DOM

242

Quiero ejecutar una función cuando se agregan algunos div o entradas al html. es posible?

Por ejemplo, se agrega una entrada de texto, luego se debe llamar a la función.

esafwan
fuente
1
A menos que algún script de terceros agregue los nodos al DOM, esto no es necesario.
Justin Johnson
duplicado de stackoverflow.com/questions/2457043/…
Gabriele Petrioli
44
@JustinJohnson si está haciendo una extensión de Chrome que inyecta código JS, es útil.
FluorescentGreen5

Respuestas:

206

Actualización de 2015, la nueva MutationObserveres compatible con los navegadores modernos:

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

Si necesita apoyar a los más antiguos, puede intentar recurrir a otros enfoques como los mencionados en esta respuesta de 5 (!) Años a continuación. Habrá dragones. Disfruta :)


¿Alguien más está cambiando el documento? Porque si tiene control total sobre los cambios, solo necesita crear su propia domChangedAPI, con una función o evento personalizado, y activarla / llamarla en cualquier lugar donde modifique cosas.

El DOM Nivel-2 tiene tipos de eventos de mutación , pero la versión anterior de IE no lo admite. Tenga en cuenta que los eventos de mutación están en desuso en la especificación de Eventos DOM3 y tienen una penalización de rendimiento .

Puede intentar emular un evento de mutación onpropertychangeen IE (y recurrir al enfoque de fuerza bruta si no hay ninguno disponible).

Para una domChange completa, un intervalo puede ser una sobremoción. Imagine que necesita almacenar el estado actual de todo el documento y examinar cada propiedad de cada elemento para que sea la misma.

Tal vez si solo le interesan los elementos y su orden (como mencionó en su pregunta), getElementsByTagName("*")puede funcionar. Esto se activará automáticamente si agrega un elemento, elimina un elemento, reemplaza elementos o cambia la estructura del documento.

Escribí una prueba de concepto:

(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);

Uso:

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

Esto funciona en IE 5.5+, FF 2+, Chrome, Safari 3+ y Opera 9.6+

galambalazs
fuente
44
Preguntándose: ¿cómo jQuery live () resuelve este problema si no pueden detectar un cambio DOM?
Kees C. Bakker
162
No puedo creer que en 2010 tuvieras IE5.5 para probar esto.
Oscar Godson
1
@JoshStodola El negrita también me estaba molestando. Decidí arreglarlo.
Bojangles
1
Los eventos de mutaciones están en desuso. Debe usar MutationObserver. He escrito mi plugin para problemas como este - github.com/AdamPietrasiak/jquery.initialize
pie6k
1
¿Cómo puedo hacer que jquery onClick dispare antes de un observador de mutaciones, que se dispara cuando se hace clic en un botón con una acción de ascua? stackoverflow.com/questions/29216434/…
SuperUberDuper el
219

Este es el enfoque final hasta ahora, con el código más pequeño:

IE9 +, FF, Webkit:

Usando MutationObserver y recurriendo a los eventos de mutación en desuso si es necesario:
(Ejemplo a continuación si solo para cambios DOM relacionados con nodos agregados o eliminados)

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
fuente
1
parece funcionar bastante bien para nuevos nodos DOM. ¿Podemos adaptarlo para manejar también los cambios del nodo dom (al menos los valores / texto del nodo DOM)
Sebastien Lorber
8
@SebastienLorber: ¿quién es "nosotros"? usted, como programador, puede tomar este código y usarlo como desee. solo lea en el MDN qué cosas puede observar el DOM y cuáles no.
vsync
2
Pase los mutations, observerparámetros a la función de devolución de llamada para obtener más control.
A1rPun
2
Esto me ayudó mucho, pero ¿cómo puedo "desatar" esto? Digamos que quiero ver un cambio solo una vez, pero ¿lo hago en varias ocasiones? oberserveDOM = nulo obviamente no funcionará ...
stiller_leser
¿Por qué esto solo funcionaría para agregar / eliminar? Parece que los eventos de mutación cubren más que eso ... developer.mozilla.org/en-US/docs/Web/Guide/Events/…
f0ster
15

Recientemente escribí un complemento que hace exactamente eso: jquery.initialize

Lo usas de la misma manera que la .eachfunción

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

La diferencia .eaches: toma su selector, en este caso, .some-elementy espera nuevos elementos con este selector en el futuro, si se agrega dicho elemento, también se inicializará.

En nuestro caso, la función de inicialización simplemente cambia el color del elemento a azul. Entonces, si agregaremos un nuevo elemento (no importa si con ajax o incluso F12 inspector o algo así) como:

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

El complemento lo iniciará al instante. Además, el complemento se asegura de que un elemento se inicialice solo una vez. Entonces, si agrega un elemento, luego .detach()lo agrega desde el cuerpo y luego lo agrega nuevamente, no se inicializará nuevamente.

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

El complemento se basa en MutationObserver: funcionará en IE9 y 10 con dependencias como se detalla en la página Léame .

pie6k
fuente
Por favor, agregue a npm.
thexpand
14

o simplemente puede crear su propio evento , que se ejecuta en todas partes

 $("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");

        });

Ejemplo completo http://jsfiddle.net/hbmaam/Mq7NX/

HB MAAM
fuente
esto no es lo mismo ... el método descrito anteriormente sigue siendo válido api.jquery.com/trigger
lefoy
10

Este es un ejemplo usando MutationObserver de Mozilla adaptado de esta publicación 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
fuente
4

Use la interfaz MutationObserver como se muestra en el 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
fuente
3
MutationObserver es JavaScript nativo, no jQuery.
Benjamin
2

¿Qué tal extender un jquery para esto?

   (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+ ha creado soporte para esto (he oído que no se ha probado).

StartCoding
fuente