Wie kann ich benachrichtigt werden, wenn der Seite ein Element hinzugefügt wird?

139

Ich möchte, dass eine Funktion meiner Wahl ausgeführt wird, wenn der Seite ein DOM-Element hinzugefügt wird. Dies steht im Zusammenhang mit einer Browsererweiterung, sodass die Webseite unabhängig von mir ausgeführt wird und ich ihre Quelle nicht ändern kann. Welche Möglichkeiten habe ich hier?

Ich denke, theoretisch könnte ich einfach setInterval()ständig nach der Anwesenheit des Elements suchen und meine Aktion ausführen, wenn das Element vorhanden ist, aber ich brauche einen besseren Ansatz.

Kevin Burke
quelle
Müssen Sie nach einem bestimmten Element suchen, das von einem anderen Skript in die Seite eingefügt wurde, oder nach einem Element, das unabhängig von der Quelle hinzugefügt wird?
Jose Faeti
1
Wissen Sie, welche Funktion das Element in den Code einer anderen Person einfügt? Wenn ja, können Sie es überschreiben und eine zusätzliche Zeile hinzufügen, die ein benutzerdefiniertes Ereignis auslöst?
Jeffreyrey
1
Mögliches Duplikat von Gibt es einen Listener für jQuery DOM-Änderungen?
Apsiller

Antworten:

86

Warnung!

Diese Antwort ist jetzt veraltet. In DOM Level 4 wurde MutationObserver eingeführt , der die veralteten Mutationsereignisse effektiv ersetzt. In dieser Antwort finden Sie eine bessere Lösung als die hier vorgestellte. Ernsthaft. Fragen Sie das DOM nicht alle 100 Millisekunden ab. Es wird CPU-Energie verschwenden und Ihre Benutzer werden Sie hassen.

Da Mutationsereignisse 2012 veraltet waren und Sie keine Kontrolle über die eingefügten Elemente haben, da sie durch den Code einer anderen Person hinzugefügt werden, besteht Ihre einzige Option darin, diese kontinuierlich zu überprüfen.

function checkDOMChange()
{
    // check for any new element being inserted here,
    // or a particular node being modified

    // call the function again after 100 milliseconds
    setTimeout( checkDOMChange, 100 );
}

Sobald diese Funktion aufgerufen wird, wird sie alle 100 Millisekunden ausgeführt, was 1/10 (ein Zehntel) einer Sekunde entspricht. Sofern Sie keine Echtzeit-Elementbeobachtung benötigen, sollte dies ausreichen.

Jose Faeti
quelle
1
jose, wie beendest du dein Settimeout, wenn die Bedingung tatsächlich erfüllt ist? dh Sie haben das Element gefunden, das schließlich x Sekunden später auf Ihre Seite geladen wurde?
Klewis
1
@blachawk Sie müssen den Rückgabewert setTimeout einer Variablen zuweisen, die Sie später als Paratemer an clearTimeout () übergeben können, um ihn zu löschen.
Jose Faeti
3
@blackhawk: Ich habe gerade diese Antwort gesehen und +1; Sie wissen jedoch, dass Sie clearTimeout () hier eigentlich nicht verwenden müssen, da setTimeout () nur einmal ausgeführt wird! Ein 'if-else' ist gerade ausreichend: Lassen Sie die Ausführung nicht auf setTimeout () übergehen, sobald Sie das eingefügte Element gefunden haben.
1111161171159459134
Eine nützliche praktische Anleitung zur Verwendung von MutationObserver(): davidwalsh.name/mutationobserver-api
gfullam
4
Das ist dreckig. Gibt es wirklich kein "Äquivalent" zu AS3 Event.ADDED_TO_STAGE?
Bitterblue
19

Zwischen der Ablehnung von Mutationsereignissen und dem Auftreten von MutationsereignissenMutationObserver bestand eine effiziente Möglichkeit, benachrichtigt zu werden, wenn ein bestimmtes Element zum DOM hinzugefügt wurde, darin, CSS3-Animationsereignisse auszunutzen .

Um den Blog-Beitrag zu zitieren:

Richten Sie eine CSS-Keyframe-Sequenz ein, die (über den CSS-Selektor Ihrer Wahl) auf alle DOM-Elemente abzielt, für die Sie ein DOM-Knoteneinfügeereignis empfangen möchten. Ich habe eine relativ harmlose und wenig genutzte CSS-Eigenschaft verwendet. Clip Ich habe Umrissfarben verwendet, um zu vermeiden, dass die beabsichtigten Seitenstile durcheinander gebracht werden. Der Code zielte einmal auf die Clip-Eigenschaft ab, ist jedoch ab Version 11 im IE nicht mehr animierbar sagte, jede Eigenschaft, die animiert werden kann, wird funktionieren, wählen Sie, welche Sie mögen.

Als Nächstes habe ich einen dokumentweiten Animationsstart-Listener hinzugefügt, den ich als Delegat zum Verarbeiten der Knoteneinfügungen verwende. Das Animationsereignis enthält eine Eigenschaft namens animationName, die angibt, welche Keyframe-Sequenz die Animation gestartet hat. Stellen Sie einfach sicher, dass die Eigenschaft animationName mit dem Namen der Keyframe-Sequenz übereinstimmt, die Sie für das Einfügen von Knoten hinzugefügt haben, und Sie können loslegen.

Scherz
quelle
Dies ist die leistungsstärkste Lösung, und es gibt eine Antwort in einer doppelten Frage , in der eine Bibliothek dafür erwähnt wird.
Dan Dascalescu
1
Unterschätzte Antwort.
Gökhan Kurt
Vorsichtig. Wenn Millisekundengenauigkeit wichtig ist, ist dieser Ansatz alles andere als genau. Diese jsbin zeigt , dass es mehr als 30 ms Unterschied zwischen einem Inline - Callback ist und mit animationstart, jsbin.com/netuquralu/1/edit .
Gajus
Ich stimme Kurt zu, das wird unterschätzt.
Zabbu
13

Sie können das livequeryPlugin für jQuery verwenden. Sie können einen Selektorausdruck wie den folgenden angeben:

$("input[type=button].removeItemButton").livequery(function () {
    $("#statusBar").text('You may now remove items.');
});

Jedes Mal, wenn eine Schaltfläche einer removeItemButtonKlasse hinzugefügt wird, wird eine Meldung in einer Statusleiste angezeigt.

In Bezug auf die Effizienz möchten Sie dies möglicherweise vermeiden, aber in jedem Fall können Sie das Plugin nutzen, anstatt Ihre eigenen Ereignishandler zu erstellen.

Überarbeitete Antwort

Die obige Antwort sollte nur erkennen, dass ein Element über das Plugin zum DOM hinzugefügt wurde .

Am wahrscheinlichsten jQuery.on()wäre jedoch ein Ansatz angemessener, zum Beispiel:

$("#myParentContainer").on('click', '.removeItemButton', function(){
          alert($(this).text() + ' has been removed');
});

Wenn Sie über dynamischen Inhalt verfügen, der beispielsweise auf Klicks reagieren soll, binden Sie Ereignisse am besten mit an einen übergeordneten Container jQuery.on.

Keine Ablenkung
quelle
11

ETA 24 Apr 17 Ich wollte dies mit etwas async/ awaitMagie ein wenig vereinfachen , da es dadurch viel prägnanter wird:

Unter Verwendung der gleichen versprochenen beobachtbaren:

const startObservable = (domNode) => {
  var targetNode = domNode;

  var observerConfig = {
    attributes: true,
    childList: true,
    characterData: true
  };

  return new Promise((resolve) => {
      var observer = new MutationObserver(function (mutations) {
         // For the sake of...observation...let's output the mutation to console to see how this all works
         mutations.forEach(function (mutation) {
             console.log(mutation.type);
         });
         resolve(mutations)
     });
     observer.observe(targetNode, observerConfig);
   })
} 

Ihre aufrufende Funktion kann so einfach sein wie:

const waitForMutation = async () => {
    const button = document.querySelector('.some-button')
    if (button !== null) button.click()
    try {
      const results = await startObservable(someDomNode)
      return results
    } catch (err) { 
      console.error(err)
    }
}

Wenn Sie eine Zeitüberschreitung hinzufügen möchten, können Sie ein einfaches Promise.raceMuster verwenden, wie hier gezeigt :

const waitForMutation = async (timeout = 5000 /*in ms*/) => {
    const button = document.querySelector('.some-button')
    if (button !== null) button.click()
    try {

      const results = await Promise.race([
          startObservable(someDomNode),
          // this will throw after the timeout, skipping 
          // the return & going to the catch block
          new Promise((resolve, reject) => setTimeout(
             reject, 
             timeout, 
             new Error('timed out waiting for mutation')
          )
       ])
      return results
    } catch (err) { 
      console.error(err)
    }
}

Original

Sie können dies ohne Bibliotheken tun, müssen jedoch einige ES6-Elemente verwenden. Beachten Sie daher die Kompatibilitätsprobleme (dh, wenn Ihre Zielgruppe hauptsächlich aus Amish-, Luddite- oder, schlimmer noch, IE8-Benutzern besteht).

Zuerst verwenden wir die MutationObserver-API , um ein Beobachterobjekt zu erstellen. Wir werden dieses Objekt in ein Versprechen einwickeln, und resolve()wenn der Rückruf ausgelöst wird (h / t davidwalshblog), wird David Walsh Blog-Artikel über Mutationen veröffentlicht :

const startObservable = (domNode) => {
    var targetNode = domNode;

    var observerConfig = {
        attributes: true,
        childList: true,
        characterData: true
    };

    return new Promise((resolve) => {
        var observer = new MutationObserver(function (mutations) {
            // For the sake of...observation...let's output the mutation to console to see how this all works
            mutations.forEach(function (mutation) {
                console.log(mutation.type);
            });
            resolve(mutations)
        });
        observer.observe(targetNode, observerConfig);
    })
} 

Dann erstellen wir eine generator function. Wenn Sie diese noch nicht verwendet haben, verpassen Sie sie - aber eine kurze Zusammenfassung lautet: Sie läuft wie eine Synchronisierungsfunktion, und wenn sie einen yield <Promise>Ausdruck findet, wartet sie nicht blockierend auf das Versprechen erfüllt ( Generatoren leisten mehr als das, aber das interessiert uns hier ).

// we'll declare our DOM node here, too
let targ = document.querySelector('#domNodeToWatch')

function* getMutation() {
    console.log("Starting")
    var mutations = yield startObservable(targ)
    console.log("done")
}

Ein schwieriger Teil bei Generatoren ist, dass sie nicht wie eine normale Funktion "zurückkehren". Wir werden also eine Hilfsfunktion verwenden, um den Generator wie eine reguläre Funktion verwenden zu können. (wieder h / t zu dwb )

function runGenerator(g) {
    var it = g(), ret;

    // asynchronously iterate over generator
    (function iterate(val){
        ret = it.next( val );

        if (!ret.done) {
            // poor man's "is it a promise?" test
            if ("then" in ret.value) {
                // wait on the promise
                ret.value.then( iterate );
            }
            // immediate value: just send right back in
            else {
                // avoid synchronous recursion
                setTimeout( function(){
                    iterate( ret.value );
                }, 0 );
            }
        }
    })();
}

Führen Sie dann zu jedem Zeitpunkt, bevor die erwartete DOM-Mutation auftreten könnte, einfach aus runGenerator(getMutation).

Jetzt können Sie DOM-Mutationen in einen synchronen Kontrollfluss integrieren. Wie wäre es damit?

Brandon
quelle
8

Es gibt eine vielversprechende Javascript-Bibliothek namens Arrive, die eine großartige Möglichkeit darstellt, die Mutationsbeobachter zu nutzen, sobald die Browserunterstützung alltäglich wird.

https://github.com/uzairfarooq/arrive/

Dave Kiss
quelle
3

Schauen Sie sich dieses Plugin an, das genau das tut - jquery.initialize

Es funktioniert genau wie jede Funktion. Der Unterschied besteht darin, dass Sie den von Ihnen eingegebenen Selektor verwenden und nach neuen Elementen suchen, die in Zukunft mit diesem Selektor übereinstimmen, und diese initialisieren

Initialisieren sieht so aus

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

Aber jetzt, wenn neues Element übereinstimmt .some-element Selektor auf der Seite angezeigt wird, wird er sofort initialisiert.

Die Art und Weise, wie neue Elemente hinzugefügt werden, ist nicht wichtig. Sie müssen sich nicht um Rückrufe usw. kümmern.

Wenn Sie also ein neues Element hinzufügen möchten, wie:

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

es wird sofort initialisiert.

Plugin basiert auf MutationObserver

pie6k
quelle
0

Eine reine Javascript-Lösung (ohne jQuery):

const SEARCH_DELAY = 100; // in ms

// it may run indefinitely. TODO: make it cancellable, using Promise's `reject`
function waitForElementToBeAdded(cssSelector) {
  return new Promise((resolve) => {
    const interval = setInterval(() => {
      if (element = document.querySelector(cssSelector)) {
        clearInterval(interval);
        resolve(element);
      }
    }, SEARCH_DELAY);
  });
}

console.log(await waitForElementToBeAdded('#main'));
vedant
quelle