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


237

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


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

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
Можете ли вы написать это, чтобы использовать вместо этого наблюдателя мутации?
— SuperUberDuper

или вы могли бы переписать это, чтобы использовать обещание?
— SuperUberDuper

25

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

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

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

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, он не будет работать, и вам, возможно, придется поискать другие ответы.


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 + и в основном все остальное (включая мобильный)


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

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

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

0

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

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

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 });
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.