Как ждать, пока элемент не существует?

237

Я работаю над расширением в Chrome, и мне интересно: как лучше узнать, когда элемент появляется? Используя простой javascript, с интервалом, который проверяет, пока элемент не существует, или jQuery имеет какой-то простой способ сделать это?

mattsven
источник
1
Похоже, что каждый вариант здесь сегодня (в том числе из комментариев) является либо устаревшим, либо неполным. Они не учитывают потрясающий ввод @ hughsk полностью, аргумент совместимости. Между тем я бы рекомендовал просто использовать обновление Брэндона для ответа Райана для общей простоты и меньшего риска накладных расходов, я полагаю.
Cregox
4
MutationObserver> DOM Mutation Events> setTimeout.
mattsven
2
Не там, где я стою. setTimeoutсовместим, прост в реализации, прост в обслуживании и имеет незначительные накладные расходы.
Cregox
setTimeout+, jQueryпо моему мнению, менее чем идеален по двум причинам: 1.) раздувание jQuery 2.) вы без необходимости вручную запрашиваете DOM для элементов, события легко справляются с этой скоростью, 3.) он всегда будет медленнее, чем любой нативный реализация. Если вам нужно что-то сделать, основываясь на наличии элемента достаточно быстро, особенно если ваша цель - беспроблемный пользовательский опыт, он уступает.
mattsven
3
Есть 3 вида людей: те, кто умеет считать, и те, кто не умеет. ; P
Cregox

Ответы:

149

DOMNodeInsertedне рекомендуется, наряду с другими событиями мутации DOM, из-за проблем с производительностью - рекомендуемый подход заключается в использовании MutationObserver для наблюдения за DOM. Это поддерживается только в новых браузерах, поэтому вы должны использовать его, DOMNodeInsertedкогда MutationObserverон недоступен.

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()
hughsk
источник
50
Я всегда находил API MutationObserver немного сложным, поэтому я собрал библиотеку Arri.js , чтобы предоставить более простой API для прослушивания создания / удаления элементов.
Uzair Farooq
15
Я рекомендую использовать превосходную библиотеку @UzairFarooq github.com/uzairfarooq/arrive
Деннис
3
Две вещи, на которые следует обратить внимание: (1) Было бы лучше сделать, if (mutation.addedNodes.length)так как if (mutation.addedNodes)все равно вернул бы true, даже если это пустой массив. (2) Вы не можете сделать это, mutation.addedNodes.forEach()потому что addNodes - это список узлов, и вы не можете перебирать список узлов с помощью forEach. Для решения этой проблемы, см. Toddmotto.com/ditch-the-array-foreach-call-nodelist-hack
thdoan
3
Можете ли вы привести пример того, как можно использовать это? Не уверен, куда поместить мой селектор jquery или код, который я хочу выполнить, когда элемент DOM существует.
Superdooperhero
1
@ Superperooro Я ответил с легким примером. Проверь это. stackoverflow.com/a/57395241/6542186
SilverSurfer
113

У меня была такая же проблема, поэтому я решил написать плагин для нее.

$(selector).waitUntilExists(function);

Код:

;(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
Спасибо за плагин. Я немного улучшил и улучшил его. Не стесняйтесь брать все, что вы хотите от моего обновления. У меня запланировано еще несколько улучшений: обновленный плагин
Брэндон Белвин
8
было бы неплохо и без jquery
dep
4
возможно, вам следует упомянуть, как это работает: он работает, спрашивая каждые 500 мс, существует ли элемент (используя a window.setInterval). Я не знаю, работает ли MutationObserverответ также путем опроса ...
спорт
2
Он не работает должным образом, если элемент уже находится на странице. Вот правильная версия этой функции: gist.github.com/PizzaBrandon/5709010
Роланд Соос
2
Можете ли вы объяснить, что толку ;в начале функции ( ;(function ($, window) {)?
mrid
77

Вот основная функция JavaScript для ожидания отображения элемента.

Параметры:

  1. selector: Эта функция ищет элемент $ {selector}
  2. time: Эта функция проверяет, существует ли этот элемент каждые миллисекунды.

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

В качестве примера настройки selector="#div1"и time=5000будем искать тег HTML, id="div1"каждые 5000 миллисекунд.

Этьен Тоннелье
источник
Ницца! Вы можете написать это так, чтобы любой селектор мог быть принят?
mattsven
Я сомневаюсь, что смогу это сделать .. Но, пожалуйста, посмотрите на этот пост, чтобы получить getElementByXpath: stackoverflow.com/questions/10596417/…
Etienne Tonnelier
1
Как насчет querySelector? developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
mattsven
1
Можете ли вы написать это, чтобы использовать вместо этого наблюдателя мутации?
SuperUberDuper
или вы могли бы переписать это, чтобы использовать обещание?
SuperUberDuper
25

Вы можете слушать DOMNodeInsertedили DOMSubtreeModifiedсобытия, которые запускаются всякий раз, когда новый элемент добавляется в DOM.

Существует также плагин LiveQuery jQuery, который определяет, когда создается новый элемент:

$("#future_element").livequery(function(){
    //element created
});
Serg
источник
1
Очень хороший плагин! Есть ли такая функция в jquery напрямую? Мне интересно, что не существует никакой функции, чтобы сделать это. И если это плагин, пожалуйста, проголосуйте за этот ответ;) Для меня это работает отлично. Большое спасибо.
Самуил
1
Примечание. IE 9 реализует DOMNodeInserted, но имеет серьезную ошибку, из-за которой он не срабатывает, когда вы добавляете элемент в течение времени, которое чаще всего требуется для его использования. Подробности по адресу: help.dottoro.com/ljmcxjla.php
mikemaccana
23

Я использовал этот подход, чтобы дождаться появления элемента, чтобы после этого я мог выполнять другие функции.

Скажем doTheRestOfTheStuff(parameters) функция должна вызываться только после того, как элемент с идентификатором the_Element_IDпоявляется или завершает загрузку, мы можем использовать,

var existCondition = setInterval(function() {
 if ($('#the_Element_ID').length) {
    console.log("Exists!");
    clearInterval(existCondition);
    doTheRestOfTheStuff(parameters);
 }
}, 100); // check every 100ms
простое число
источник
21

Ты можешь сделать

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

});

Обратите внимание, что это будет работать, только если элемент присутствует в DOM при запросе с сервера. Если элемент добавляется динамически через JavaScript, он не будет работать, и вам, возможно, придется поискать другие ответы.

Splynx
источник
7
В .ready()функции работает для большинства ничего (если не что - нибудь), а не просто document. Это просто не будет работать с динамически созданными элементами, даже на .live().
Ричард Нил Илаган
7
@Bery, как отметил Ричард, работает только для элементов, которые уже присутствуют в HTML, когда он впервые запрашивается с сервера. Если Javascript используется для динамического добавления элемента в DOM, он не работает.
Чандраншу
6
@ Сам, не могли бы вы уточнить, как прикрепить его к ссылке на элемент в памяти?
Викас Сингхал
3
Этот ответ неверен. То, что вы на самом деле здесь проверяете, это обычный $(document).ready()элемент, а не тот элемент, который, как вы думаете, будет применяться. Вот как работает этот специальный слушатель. Пример
Shikkediel
1
Это использование не рекомендуется в соответствии с api.jquery.com/ready
splintor
14

Я думаю, что до сих пор нет никакого ответа здесь с легким и читаемым рабочим примером. Используйте MutationObserver interface для обнаружения изменений DOM, например:

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>

Примечание: документы на испанском языке MozillaMutationObserver более подробны, если вам нужна дополнительная информация.

Серебряный Серфер
источник
2
Оставьте комментарий, объясняющий причину отрицательного голоса, чтобы я мог улучшить свой ответ. Спасибо.
SilverSurfer
12

Просто добавьте нужный селектор. Как только элемент найден, вы можете получить доступ к функции обратного вызова.

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
PossessWithin согласен, это очень чистое решение и работает для меня.
Jstafford
3
Этот ответ работает как на IE8-10, так и на современных браузерах. Основная проблема заключается в том, что он будет продолжать работать, если элемент не существует, поэтому лучше всего, когда вы уверены, что элемент будет там. В противном случае вы можете добавить счетчик.
Для имени
1
Отлично
Джеймс Стюарт
1
Работал как шарм !!
Аман
1
Они были похожи, не идентичны. Кроме того, многие люди делают то же самое. Наконец, я сам кодировал это решение. Это плохая аргументация, однако, даже если бы это было действительно так, я был бы признателен за комментарий, дающий мне знать. Ответ решает проблему ОП и не имеет видимых мотивов для опровержения.
Диего Фортес
11

Для простого подхода с использованием jQuery я обнаружил, что это хорошо работает:

  // 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");
    });
  });

Здесь мы просто проверяем каждые 500 мс, чтобы увидеть, загружен ли элемент, когда он есть, мы можем его использовать.

Это особенно полезно для добавления обработчиков кликов к элементам, которые были динамически добавлены в документ.

Хедли смит
источник
8

Как насчет вставки Query библиотеки ?

inserttionQuery использует обратные вызовы CSS-анимации, прикрепленные к селекторам, указанным для запуска обратного вызова при создании элемента. Этот метод позволяет выполнять обратные вызовы при каждом создании элемента, а не только в первый раз.

Из GitHub:

Не дом-событие способ поймать обнаружение узлов. И он использует селекторы.

Это не только для более широкой поддержки браузера, это может быть лучше, чем DOMMutationObserver для определенных вещей.

Зачем?

  • Поскольку события DOM замедляют работу браузера, а inserttionQuery - нет
  • Потому что DOM Mutation Observer имеет меньшую поддержку браузера, чем inserttionQuery
  • Потому что с inserttionQuery вы можете фильтровать изменения DOM с помощью селекторов без снижения производительности!

Широкая поддержка!

IE10 + и в основном все остальное (включая мобильный)

b3wii
источник
7

Вот функция, которая действует как тонкая оболочка вокруг MutationObserver. Единственное требование - браузер должен поддерживать MutationObserver; нет зависимости от JQuery. Запустите фрагмент ниже, чтобы увидеть рабочий пример.

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

Вот решение с возвратом обещаний в ванильном Javascript (без грязных обратных вызовов). По умолчанию он проверяет каждые 200 мс.

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

Вот чистая функция Javascript, которая позволяет вам ждать чего угодно. Установите интервал, чтобы он занимал меньше ресурсов процессора.

/**
 * @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);
}

Чтобы вызвать это, например, в jQuery, используйте что-то вроде:

waitUntil(function(){
    return $('#myelement').length > 0;
}, function(){
    alert("myelement now exists");
}, function(){
    alert("I'm bored. I give up.");
});
xgretsch
источник
3

Решение, возвращающее Promiseи позволяющее использовать тайм-аут (совместимо с IE 11+).

Для одного элемента (тип Элемент):

"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);
    });
}

Для нескольких элементов (тип 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);
    });
}

Примеры:

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

Работает как для списка элементов, так и для одного элемента.

Анвар
источник
1
Мое любимое решение! Зачем проверять element instanceof HTMLElement? Может ли это быть когда-нибудь, кроме nullили HTMLElement?
Leeroy
1
Вы поднимаете интересный вопрос. Я должен был сделать его шире, используя Elementвместо этого (исправлено). Я просто делаю проверку, потому что хочу убедиться, что у переменной elementесть свойство, innerHTMLкак указано в документации Element MDN . Не стесняйтесь удалить его, если вам все равно!
Анвар
2

Более чистый пример использования MutationObserver:

new MutationObserver( mutation => {
    if (!mutation.addedNodes) return
    mutation.addedNodes.forEach( node => {
        // do stuff with node
    })
})
Zaz
источник
2

Это простое решение для тех, кто привык к обещаниям и не хочет использовать какие-либо сторонние библиотеки или таймеры.

Я использовал его в своих проектах некоторое время

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

Чтобы использовать это:

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

или с асинхронным ожиданием

const elm = await waitForElm('.some-classs')
Ён Ван
источник
Это аккуратно! Самое интересное в этом то, что вы можете использовать его с async/ awaitтоже. Вы могли бы также быть в состоянии выжать больше производительности из него делаютmutations.addedNodes.find(node => node.matchesSelector("..."))
mattsven
@mattsven Хорошая мысль! Проверка только узлов в мутациях является более производительной, чем выполнение document.querySelector.
Йонг Ван
Пожалуйста, исправьте орфографическую ошибку, watiForElm to waitForElm
dalvir
1

Если вы хотите, чтобы через некоторое время (тайм-аут) он перестал искать, то будет работать следующий jQuery. Время ожидания истечет через 10 секунд. Мне нужно было использовать этот код, а не чистый JS, потому что мне нужно было выбрать вход по имени, и у меня были проблемы с реализацией некоторых других решений.

 // 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);
S-Томас
источник
0

Я обычно использую этот фрагмент для диспетчера тегов:

<script>
(function exists() {
  if (!document.querySelector('<selector>')) {
    return setTimeout(exists);
  }
  // code when element exists
})();  
</script>
Alejo JM
источник
0

если у вас есть асинхронные изменения в домене, эта функция проверяет (с ограничением по времени в секундах) на наличие элементов DOM, это не будет тяжелым для DOM и его на основе Promise :)

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 });
Мени Эдри
источник