Come aspettare fino a quando esiste un elemento?


237

Sto lavorando su un'estensione in Chrome e mi chiedo: qual è il modo migliore per scoprire quando un elemento nasce? Usando javascript semplice, con un intervallo che controlla fino a quando esiste un elemento, o jQuery ha un modo semplice per farlo?


1
Sembra che ogni singola opzione qui oggi (anche dai commenti) sia obsoleta o incompleta. Non considerano pienamente l'impressionante input di @ hughsk, l'argomento della compatibilità. Nel frattempo raccomanderei semplicemente di utilizzare l'aggiornamento di Brandon sulla risposta di Ryan per semplicità generale e meno rischi di spese generali, suppongo.
Cregox,

4
MutationObserver> DOM Mutation Events> setTimeout.
mattsven,

2
Non da dove mi trovo. setTimeoutè compatibile, semplice da implementare, semplice da mantenere e con costi generali trascurabili.
Cregox,

setTimeout+ jQueryè meno che ideale a mio parere per due motivi: 1.) jQuery bloat 2.) stai inutilmente interrogando manualmente il DOM per gli elementi, gli eventi battono facilmente quella velocità, 3.) sarà sempre più lento di qualsiasi nativo implementazione. Se devi fare qualcosa in base alla presenza di un elemento ragionevolmente rapidamente, specialmente se il tuo obiettivo è un'esperienza utente senza soluzione di continuità, è inferiore.
mattsven,

3
Esistono 3 tipi di persone: quelli che possono contare e quelli che non possono. ; P
cregox,

Risposte:


149

DOMNodeInsertedviene deprecato, insieme agli altri eventi di mutazione DOM, a causa di problemi di prestazioni: l'approccio consigliato consiste nell'utilizzare un MutationObserver per guardare il DOM. È supportato solo nei browser più recenti, quindi dovresti ricorrere a DOMNodeInsertedquando MutationObservernon è disponibile.

var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    if (!mutation.addedNodes) return

    for (var i = 0; i < mutation.addedNodes.length; i++) {
      // do things to your newly added nodes here
      var node = mutation.addedNodes[i]
    }
  })
})

observer.observe(document.body, {
    childList: true
  , subtree: true
  , attributes: false
  , characterData: false
})

// stop watching using:
observer.disconnect()

50
Ho sempre trovato MutationObserver api un po 'complesso, quindi ho creato una libreria, arrived.js , per fornire un api più semplice per ascoltare la creazione / rimozione di elementi.
Uzair Farooq,

15
Consiglio di utilizzare @UzairFarooq libreria eccellente github.com/uzairfarooq/arrive
Dennis

3
Due cose da notare: (1) Sarebbe meglio fare if (mutation.addedNodes.length)poiché if (mutation.addedNodes)sarebbe comunque vero anche se si tratta di un array vuoto. (2) Non è possibile farlo mutation.addedNodes.forEach()poiché AddedNodes è un elenco di nodi e non è possibile scorrere una lista di nodi con forEach. Per una soluzione a questo, vedi toddmotto.com/ditch-the-array-foreach-call-nodelist-hack
thdoan

3
Puoi fare un esempio di come si userebbe questo? Non sono sicuro di dove posizionare il selettore jquery o il codice che voglio eseguire quando esiste l'elemento DOM.
Superdooperhero,

1
@Superdooperhero Ho fatto una risposta con un semplice esempio. Controllalo. stackoverflow.com/a/57395241/6542186
SilverSurfer

113

Stavo avendo lo stesso problema, quindi sono andato avanti e ho scritto un plugin per questo.

$(selector).waitUntilExists(function);

Codice:

;(function ($, window) {

var intervals = {};
var removeListener = function(selector) {

    if (intervals[selector]) {

        window.clearInterval(intervals[selector]);
        intervals[selector] = null;
    }
};
var found = 'waitUntilExists.found';

/**
 * @function
 * @property {object} jQuery plugin which runs handler function once specified
 *           element is inserted into the DOM
 * @param {function|string} handler 
 *            A function to execute at the time when the element is inserted or 
 *            string "remove" to remove the listener from the given selector
 * @param {bool} shouldRunHandlerOnce 
 *            Optional: if true, handler is unbound after its first invocation
 * @example jQuery(selector).waitUntilExists(function);
 */

$.fn.waitUntilExists = function(handler, shouldRunHandlerOnce, isChild) {

    var selector = this.selector;
    var $this = $(selector);
    var $elements = $this.not(function() { return $(this).data(found); });

    if (handler === 'remove') {

        // Hijack and remove interval immediately if the code requests
        removeListener(selector);
    }
    else {

        // Run the handler on all found elements and mark as found
        $elements.each(handler).data(found, true);

        if (shouldRunHandlerOnce && $this.length) {

            // Element was found, implying the handler already ran for all 
            // matched elements
            removeListener(selector);
        }
        else if (!isChild) {

            // If this is a recurring search or if the target has not yet been 
            // found, create an interval to continue searching for the target
            intervals[selector] = window.setInterval(function () {

                $this.waitUntilExists(handler, shouldRunHandlerOnce, true);
            }, 500);
        }
    }

    return $this;
};

}(jQuery, window));

5
Grazie per il plugin. L'ho biforcuta e migliorata un po '. Sentiti libero di prendere quello che vuoi dal mio aggiornamento. Ho in programma ancora qualche miglioramento: plug-in aggiornato
Brandon Belvin,

8
sarebbe bello anche senza jquery dep ...;)
knutole

4
forse dovresti menzionare come funziona: funziona chiedendo ogni 500 ms se l'elemento esiste (usando a window.setInterval). Non so se la MutationObserverrisposta funzioni anche tramite polling ...
sport

2
Non funziona correttamente se l'elemento è già nella pagina. Ecco la versione corretta di questa funzione: gist.github.com/PizzaBrandon/5709010
Roland Soós

2
Puoi spiegare a cosa serve ;all'inizio della funzione ( ;(function ($, window) {)?
mrid,

76

Ecco una funzione JavaScript di base per attendere la visualizzazione di un elemento.

parametri:

  1. selector: Questa funzione cerca l'elemento $ {selector}
  2. time: Questa funzione controlla se questo elemento esiste ogni $ {tempo} millisecondi.

    function waitForElementToDisplay(selector, time) {
            if(document.querySelector(selector)!=null) {
                alert("The element is displayed, you can put your code instead of this alert.")
                return;
            }
            else {
                setTimeout(function() {
                    waitForElementToDisplay(selector, time);
                }, time);
            }
        }

Ad esempio, imposta selector="#div1"e time=5000cercherà il tag HTML di cui id="div1"ogni 5000 millisecondi.


Bello! Puoi scriverlo in modo che qualsiasi selettore possa essere accettato?
mattsven,

Dubito che posso farlo .. Ma si prega di dare un'occhiata a questo post per ottenere il getElementByXpath: stackoverflow.com/questions/10596417/...
Etienne Tonnelier


1
Puoi scriverlo per usare invece l'osservatore delle mutazioni?
SuperUberDuper

o potresti riscriverlo per usare una promessa?
SuperUberDuper

25

Puoi ascoltare DOMNodeInsertedo DOMSubtreeModifiedeventi che si attivano ogni volta che un nuovo elemento viene aggiunto al DOM.

Esiste anche il plug-in LiveQuery jQuery che rileverà quando viene creato un nuovo elemento:

$("#future_element").livequery(function(){
    //element created
});

1
Plugin molto bello! Esiste una funzione simile direttamente in jquery? Mi chiedo che non ci siano funzionalità esistenti per farlo. E se questo è IL plugin, per favore vota per questa risposta;) Per me, funziona perfettamente. Grazie mille.
Samuel,

1
Nota IE 9 implementa DOMNodeInserted ma presenta un bug importante in cui non si attiva quando si aggiunge un elemento per il momento, che è il più delle volte quando si desidera utilizzarlo. I dettagli sono disponibili su: help.dottoro.com/ljmcxjla.php
mikemaccana,

23

Ho usato questo approccio per aspettare che appaia un elemento in modo da poter eseguire successivamente le altre funzioni.

Supponiamo che la doTheRestOfTheStuff(parameters)funzione debba essere chiamata solo dopo la visualizzazione dell'elemento con ID the_Element_IDo il caricamento completato, possiamo usare,

var existCondition = setInterval(function() {
 if ($('#the_Element_ID').length) {
    console.log("Exists!");
    clearInterval(existCondition);
    doTheRestOfTheStuff(parameters);
 }
}, 100); // check every 100ms

21

Tu puoi fare

$('#yourelement').ready(function() {

});

Si noti che questo funzionerà solo se l'elemento è presente nel DOM quando viene richiesto dal server. Se l'elemento viene aggiunto in modo dinamico tramite JavaScript, non funzionerà e potrebbe essere necessario esaminare le altre risposte.


7
La .ready()funzione funziona per quasi tutto (se non nulla), non solo document. Semplicemente non funzionerà con elementi creati dinamicamente, nemmeno su .live().
Richard Neil Ilagan,

7
@Bery, come ha sottolineato Richard, funziona solo per elementi che sono già presenti nell'HTML quando viene richiesto per la prima volta dal server. Se Javascript viene utilizzato per aggiungere un elemento in modo dinamico al DOM, non funziona.
Chandranshu,

6
@Sam, puoi chiarire come collegarlo al riferimento dell'elemento in memoria?
Vikas Singhal,

3
Questa risposta non è corretta Quello che stai effettivamente controllando qui è un normale $(document).ready(), non l'elemento che ritieni possa essere applicato anche. Ecco come funziona questo ascoltatore speciale . Esempio
Shikkediel,

1
Questo utilizzo non è raccomandato secondo api.jquery.com/ready
splintor

14

Penso che ancora non ci sia alcuna risposta qui con un esempio di lavoro facile e leggibile. Utilizzare MutationObserver interface per rilevare le modifiche DOM, in questo modo:

var observer = new MutationObserver(function(mutations) {
    if ($("p").length) {
        console.log("Exist, lets do something");
        observer.disconnect(); 
        //We can disconnect observer once the element exist if we dont want observe more changes in the DOM
    }
});

// Start observing
observer.observe(document.body, { //document.body is node target to observe
    childList: true, //This is a must have for the observer with subtree
    subtree: true //Set to true if changes must also be observed in descendants.
});
            
$(document).ready(function() {
    $("button").on("click", function() {
        $("p").remove();
        setTimeout(function() {
            $("#newContent").append("<p>New element</p>");
        }, 2000);
    });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<button>New content</button>
<div id="newContent"></div>

Nota: i documenti spagnoli su MozillaMutationObserver sono più dettagliati se vuoi maggiori informazioni.


2
Considera di lasciare un commento che spieghi il motivo del downvote, così posso migliorare la mia risposta. Grazie.
SilverSurfer

12

Aggiungi semplicemente il selettore che desideri. Una volta trovato l'elemento, è possibile accedere alla funzione di richiamata.

const waitUntilElementExists = (selector, callback) => {
const el = document.querySelector(selector);

if (el){
    return callback(el);
}

setTimeout(() => waitUntilElementExists(selector, callback), 500);
}

waitUntilElementExists('.wait-for-me', (el) => console.log(el));

2
Possesso: D'accordo, questa è una soluzione molto pulita e funziona per me.
jstafford,

3
Questa risposta funziona su IE8-10 e sui browser moderni. Il problema principale è che continuerà a funzionare se l'elemento non esiste, quindi è meglio quando sei sicuro che l'elemento sarà lì. Altrimenti, potresti aggiungere un contatore.
Per il nome

1
Ha funzionato perfettamente per me
James Stewart

1
Ha funzionato come un incanto !!
Aman,

1
Erano simili, non identici. Inoltre, molte persone stanno facendo lo stesso. Infine, ho codificato questa soluzione da solo. Questo è un ragionamento scadente, tuttavia, anche se fosse davvero il caso, apprezzerei un commento che mi faccia sapere. La risposta risolve il problema di OP e non ha motivi apparenti da sottoporre a voto negativo.
Diego Fortes,

11

Per un semplice approccio usando jQuery ho scoperto che funziona bene:

  // Wait for element to exist.
  function elementLoaded(el, cb) {
    if ($(el).length) {
      // Element is now loaded.
      cb($(el));
    } else {
      // Repeat every 500ms.
      setTimeout(function() {
        elementLoaded(el, cb)
      }, 500);
    }
  };

  elementLoaded('.element-selector', function(el) {
    // Element is ready to use.
    el.click(function() {
      alert("You just clicked a dynamically inserted element");
    });
  });

Qui controlliamo semplicemente ogni 500 ms per vedere se l'elemento è caricato, quando lo è, possiamo usarlo.

Ciò è particolarmente utile per l'aggiunta di gestori di clic agli elementi che sono stati aggiunti dinamicamente al documento.


8

Che ne dici della libreria insertionQuery ?

insertionQuery utilizza i callback di animazione CSS associati ai selettori specificati per eseguire un callback quando viene creato un elemento. Questo metodo consente di eseguire callback ogni volta che viene creato un elemento, non solo la prima volta.

Da github:

Modo non-dom-event per catturare la visualizzazione dei nodi. E utilizza selettori.

Non è solo per un supporto più ampio del browser, può essere migliore di DOMMutationObserver per certe cose.

Perché?

  • Perché DOM Events rallenta il browser e insertionQuery no
  • Perché DOM Mutation Observer ha meno supporto del browser rispetto a insertionQuery
  • Perché con insertionQuery puoi filtrare le modifiche DOM usando i selettori senza sovraccarico di prestazioni!

Supporto diffuso!

IE10 + e soprattutto qualsiasi altra cosa (incluso il cellulare)


7

Ecco una funzione che funge da thin wrapper per MutationObserver. L'unico requisito è che il browser supporti MutationObserver; non c'è dipendenza da JQuery. Esegui lo snippet di seguito per vedere un esempio funzionante.

function waitForMutation(parentNode, isMatchFunc, handlerFunc, observeSubtree, disconnectAfterMatch) {
  var defaultIfUndefined = function(val, defaultVal) {
    return (typeof val === "undefined") ? defaultVal : val;
  };

  observeSubtree = defaultIfUndefined(observeSubtree, false);
  disconnectAfterMatch = defaultIfUndefined(disconnectAfterMatch, false);

  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      if (mutation.addedNodes) {
        for (var i = 0; i < mutation.addedNodes.length; i++) {
          var node = mutation.addedNodes[i];
          if (isMatchFunc(node)) {
            handlerFunc(node);
            if (disconnectAfterMatch) observer.disconnect();
          };
        }
      }
    });
  });

  observer.observe(parentNode, {
    childList: true,
    attributes: false,
    characterData: false,
    subtree: observeSubtree
  });
}

// Example
waitForMutation(
  // parentNode: Root node to observe. If the mutation you're looking for
  // might not occur directly below parentNode, pass 'true' to the
  // observeSubtree parameter.
  document.getElementById("outerContent"),
  // isMatchFunc: Function to identify a match. If it returns true,
  // handlerFunc will run.
  // MutationObserver only fires once per mutation, not once for every node
  // inside the mutation. If the element we're looking for is a child of
  // the newly-added element, we need to use something like
  // node.querySelector() to find it.
  function(node) {
    return node.querySelector(".foo") !== null;
  },
  // handlerFunc: Handler.
  function(node) {
    var elem = document.createElement("div");
    elem.appendChild(document.createTextNode("Added node (" + node.innerText + ")"));
    document.getElementById("log").appendChild(elem);
  },
  // observeSubtree
  true,
  // disconnectAfterMatch: If this is true the hanlerFunc will only run on
  // the first time that isMatchFunc returns true. If it's false, the handler
  // will continue to fire on matches.
  false);

// Set up UI. Using JQuery here for convenience.

$outerContent = $("#outerContent");
$innerContent = $("#innerContent");

$("#addOuter").on("click", function() {
  var newNode = $("<div><span class='foo'>Outer</span></div>");
  $outerContent.append(newNode);
});
$("#addInner").on("click", function() {
  var newNode = $("<div><span class='foo'>Inner</span></div>");
  $innerContent.append(newNode);
});
.content {
  padding: 1em;
  border: solid 1px black;
  overflow-y: auto;
}
#innerContent {
  height: 100px;
}
#outerContent {
  height: 200px;
}
#log {
  font-family: Courier;
  font-size: 10pt;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h2>Create some mutations</h2>
<div id="main">
  <button id="addOuter">Add outer node</button>
  <button id="addInner">Add inner node</button>
  <div class="content" id="outerContent">
    <div class="content" id="innerContent"></div>
  </div>
</div>
<h2>Log</h2>
<div id="log"></div>


6

Ecco una soluzione di ritorno promessa in Javascript vanilla (nessun callback disordinato). Di default controlla ogni 200ms.

function waitFor(selector) {
    return new Promise(function (res, rej) {
        waitForElementToDisplay(selector, 200);
        function waitForElementToDisplay(selector, time) {
            if (document.querySelector(selector) != null) {
                res(document.querySelector(selector));
            }
            else {
                setTimeout(function () {
                    waitForElementToDisplay(selector, time);
                }, time);
            }
        }
    });
}

5

Ecco una pura funzione Javascript che ti consente di aspettare qualsiasi cosa. Impostare l'intervallo più lungo per richiedere meno risorse della CPU.

/**
 * @brief Wait for something to be ready before triggering a timeout
 * @param {callback} isready Function which returns true when the thing we're waiting for has happened
 * @param {callback} success Function to call when the thing is ready
 * @param {callback} error Function to call if we time out before the event becomes ready
 * @param {int} count Number of times to retry the timeout (default 300 or 6s)
 * @param {int} interval Number of milliseconds to wait between attempts (default 20ms)
 */
function waitUntil(isready, success, error, count, interval){
    if (count === undefined) {
        count = 300;
    }
    if (interval === undefined) {
        interval = 20;
    }
    if (isready()) {
        success();
        return;
    }
    // The call back isn't ready. We need to wait for it
    setTimeout(function(){
        if (!count) {
            // We have run out of retries
            if (error !== undefined) {
                error();
            }
        } else {
            // Try again
            waitUntil(isready, success, error, count -1, interval);
        }
    }, interval);
}

Per chiamare questo, ad esempio in jQuery, utilizzare qualcosa del tipo:

waitUntil(function(){
    return $('#myelement').length > 0;
}, function(){
    alert("myelement now exists");
}, function(){
    alert("I'm bored. I give up.");
});

3

Una soluzione che restituisce un Promisee che consente di utilizzare un timeout (compatibile con IE 11+).

Per un singolo elemento (digitare Element):

"use strict";

function waitUntilElementLoaded(selector) {
    var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

    var start = performance.now();
    var now = 0;

    return new Promise(function (resolve, reject) {
        var interval = setInterval(function () {
            var element = document.querySelector(selector);

            if (element instanceof Element) {
                clearInterval(interval);

                resolve();
            }

            now = performance.now();

            if (now - start >= timeout) {
                reject("Could not find the element " + selector + " within " + timeout + " ms");
            }
        }, 100);
    });
}

Per più elementi (digitare NodeList):

"use strict";

function waitUntilElementsLoaded(selector) {
    var timeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

    var start = performance.now();
    var now = 0;

    return new Promise(function (resolve, reject) {
        var interval = setInterval(function () {
            var elements = document.querySelectorAll(selector);

            if (elements instanceof NodeList) {
                clearInterval(interval);

                resolve(elements);
            }

            now = performance.now();

            if (now - start >= timeout) {
                reject("Could not find elements " + selector + " within " + timeout + " ms");
            }
        }, 100);
    });
}

Esempi:

waitUntilElementLoaded('#message', 800).then(function(element) {
    // element found and available

    element.innerHTML = '...';
}).catch(function() {
    // element not found within 800 milliseconds
});

waitUntilElementsLoaded('.message', 10000).then(function(elements) {
    for(const element of elements) {
        // ....
    }
}).catch(function(error) {
    // elements not found withing 10 seconds
});

Funziona sia per un elenco di elementi che per un singolo elemento.


1
La mia soluzione preferita! Perché controllare element instanceof HTMLElement? Può mai essere altro che nullo HTMLElement?
Leeroy,

1
Sollevi un punto interessante. Avrei dovuto ampliarlo usando Elementinvece (risolto). Faccio solo il controllo perché voglio essere sicuro che la variabile elementabbia la proprietà innerHTMLcome afferma la documentazione di Element MDN . Sentiti libero di rimuoverlo se non ti interessa!
Anwar,

2

Un esempio più pulito usando MutationObserver:

new MutationObserver( mutation => {
    if (!mutation.addedNodes) return
    mutation.addedNodes.forEach( node => {
        // do stuff with node
    })
})

2

Questa è una soluzione semplice per coloro che sono abituati alle promesse e non vogliono usare librerie o timer di terze parti.

Lo sto usando nei miei progetti da un po '

function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                resolve(document.querySelector(selector));
                observer.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

Per usarlo:

waitForElm('.some-class').then(elm => console.log(elm.textContent));

o con asincrono / attendi

const elm = await waitForElm('.some-classs')

Questo è pulito! La parte interessante è che puoi usarlo anche con async/ await. Potresti anche essere in grado di spremere più prestazioni facendomutations.addedNodes.find(node => node.matchesSelector("..."))
mattsven

@mattsven Un buon punto! Il controllo dei soli nodi nelle mutazioni è più efficace rispetto a document.querySelector.
Yong Wang,

Correggi l'errore di ortografia, watiForElm to waitForElm
dalvir

1

Se vuoi che smetta di occuparsi di un po '(timeout), il seguente jQuery funzionerà. Scadrà dopo 10 secondi. Avevo bisogno di usare questo codice piuttosto che JS puro perché avevo bisogno di selezionare un input tramite nome e avevo problemi a implementare alcune delle altre soluzioni.

 // Wait for element to exist.

    function imageLoaded(el, cb,time) {

        if ($(el).length) {
            // Element is now loaded.

            cb($(el));

            var imageInput =  $('input[name=product\\[image_location\\]]');
            console.log(imageInput);

        } else if(time < 10000) {
            // Repeat every 500ms.
            setTimeout(function() {
               time = time+500;

                imageLoaded(el, cb, time)
            }, 500);
        }
    };

    var time = 500;

    imageLoaded('input[name=product\\[image_location\\]]', function(el) {

     //do stuff here 

     },time);

0

Di solito uso questo frammento per Tag Manager:

<script>
(function exists() {
  if (!document.querySelector('<selector>')) {
    return setTimeout(exists);
  }
  // code when element exists
})();  
</script>

0

se si dispone di modifiche al dom asincrono, questa funzione controlla (con limite di tempo in secondi) per gli elementi DOM, non sarà pesante per il DOM e la sua base Promessa :)

function getElement(selector, i = 5) {
  return new Promise(async (resolve, reject) => {
    if(i <= 0) return reject(`${selector} not found`);
    const elements = document.querySelectorAll(selector);
    if(elements.length) return resolve(elements);
    return setTimeout(async () => await getElement(selector, i-1), 1000);
  })
}

// Now call it with your selector

try {
  element = await getElement('.woohoo');
} catch(e) { // catch the e }

//OR

getElement('.woohoo', 5)
.then(element => { // do somthing with the elements })
.catch(e => { // catch the error });
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.