当页面添加了一个元素时,我该如何收到通知?

167

当DOM元素添加到页面时,我希望能运行我选择的某个功能。这是在浏览器扩展上下文中,所以网页独立于我运行,我不能修改它的源代码。在这里我有什么选择?

我想,理论上,我可以使用setInterval()不断搜索元素的存在,并在元素出现时执行我的操作,但我需要更好的方法。


你需要检查页面中另一个脚本放置的特定元素,还是任何添加的元素都要检查,无论来源如何? - Jose Faeti
1
你知道在别人的代码中哪个函数添加元素吗?如果知道,你可以覆盖它并添加一个额外的行来触发自定义事件。 - jeffreydev
1
可能是重复的问题:是否有jQuery DOM更改监听器? - apsillers
这个回答解决了你的问题吗?确定HTML元素是否已经动态添加到DOM中 - jpaugh
重复投票是因为被接受的答案推荐并链接到了另一个解决方案,该解决方案在此处提供:https://dev59.com/j2Qn5IYBdhLWcg3wt5Eg - jpaugh
8个回答

97

警告!

这个答案已经过时了。DOM Level 4 引入了MutationObserver,提供了一个有效的替代方案来取代被弃用的变异事件。请参考这个回答中提供的更好的解决方案。真的,请不要每隔100毫秒轮询 DOM;这会浪费 CPU 资源,并且你的用户会讨厌你。

自从2012年开始废弃变异事件以来,如果你没有控制插入的元素,因为它们是由别人的代码添加的,那么你唯一的选择就是不断地检查它们。

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

调用此函数后,它将每隔100毫秒运行一次,即1/10(十分之一)秒。除非您需要实时元素观察,否则这应该足够了。


1
Jose,当条件实际满足时,你如何结束你的setTimeout?也就是说,你找到了最终在你的页面上加载x秒钟的元素? - klewis
3
@blachawk,你需要将setTimeout的返回值赋值给一个变量,这个变量之后可以作为参数传递给clearTimeout()来清除定时器。 - Jose Faeti
3
@blackhawk:我刚看到了那个答案并点赞了它;但你需要知道,在这里实际上不需要使用 clearTimeout(),因为 setTimeout() 只会运行一次!一个“if-else”就足够了:一旦找到插入的元素,就不要让执行继续传递到 setTimeout()。 - 1111161171159459134
1
使用MutationObserver()的实用实践指南:https://davidwalsh.name/mutationobserver-api - gfullam
5
那很脏。真的没有AS3的Event.ADDED_TO_STAGE的“等价物”吗? - Bitterblue
显示剩余6条评论

47

22
突变事件被弃用和MutationObserver出现之间,一个有效的方法来通知特定元素已被添加到DOM中是通过利用CSS3动画事件
引用博客文章的话:
设置一个CSS关键帧序列,通过您选择的CSS选择器来定位任何需要接收DOM节点插入事件的DOM元素。我使用了一个相对温和且很少使用的CSS属性clip,但为了避免影响页面样式,我改用outline-color - 代码曾经定位到clip属性,但自IE 11版本以来无法进行动画处理。话虽如此,任何可进行动画处理的属性都可以使用,选择您喜欢的即可。
接下来,我添加了一个文档范围的animationstart监听器,用作委托来处理节点插入。动画事件具有一个名为animationName的属性,告诉您哪个关键帧序列启动了动画。只需确保animationName属性与添加用于节点插入的关键帧序列名称相同即可。

这是最高性能的解决方案,并且在一个重复的问题中有一个答案,提到了一个库来解决这个问题。 - Dan Dascalescu
1
被低估的答案。 - Gokhan Kurt
小心。如果毫秒级精度很重要,那么这种方法远非准确。 这个 jsbin 演示了直接使用回调函数和使用 animationstart 之间超过30毫秒的差异,https://jsbin.com/netuquralu/1/edit。 - Gajus
我同意 Kurt 的观点,这个东西被低估了。 - Zabbu
请确保该元素没有应用'display:none;'样式,否则该元素将无法渲染,因此即使您在检查时看到该元素存在,也不能发挥作用。 - Mihir

17

预计于 4 月 24 日完成 我想通过一些 async/await 的魔法来简化这个过程,因为它使得代码更加简洁:

使用相同的 promisified-observable:

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

你的调用函数可以非常简单,例如:

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

如果您想添加一个超时时间,可以使用简单的 Promise.race 模式,如此示例所示:

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

Translated

您可以在不使用库的情况下完成这项任务,但您需要使用一些ES6技术,因此需要注意兼容性问题(即,如果您的受众主要是Amish、卢德派,或更糟糕的是IE8用户)。

首先,我们将使用MutationObserver API构建一个观察者对象。我们将把这个对象封装在一个Promise中,并在回调被触发时使用resolve()(参考davidwalshblog)david walsh有关修改的文章:

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

然后,我们将创建一个生成器函数。 如果您还没有使用过这些功能,则会错过一些东西--但简短的摘要是:它像同步函数一样运行,并且当它找到一个yield <Promise>表达式时,它以非阻塞方式等待承诺被实现(生成器可以做更多的事情,但这是我们在这里感兴趣的内容)。

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

关于生成器的一个棘手问题是它们不像普通函数一样“返回”。因此,我们将使用一个辅助函数来使生成器能够像常规函数一样使用。(再次致谢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 );
            }
        }
    })();
}

那么,在预期 DOM 变化可能发生之前的任何时候,只需运行 runGenerator(getMutation)

现在,您可以将 DOM 变化集成到同步风格的控制流中。这不错吧。


13

(请参见底部的更新答案)

你可以使用jQuery的livequery插件。你可以提供一个选择器表达式,例如:

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

每次添加一个 removeItemButton 类的按钮时,都会在状态栏中出现一条消息。

就效率而言,您可能希望避免这种情况,但无论如何,您都可以利用插件而不是创建自己的事件处理程序。

重新访问的答案

上面的答案只是用来检测通过插件是否已经将项目添加到DOM中。

然而,很可能,jQuery.on() 方法更为合适,例如:

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

如果您有需要响应点击事件的动态内容,最好使用 jQuery.on 将事件绑定到父容器。


5

请查看这个插件,它可以完美实现您所需的功能 - jquery.initialize

它的工作方式与.each函数完全相同,唯一的区别是它接受您输入的选择器,并监视以后匹配此选择器的新项目并对其进行初始化。

Initialize的示例代码如下:

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

但是,如果页面上出现了与.some-element选择器匹配的新元素,则会立即初始化该元素。

添加新项的方式并不重要,您不需要关心任何回调等。

因此,如果您添加了新元素,例如:

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

它将立即初始化。

插件基于MutationObserver


1
一个纯JavaScript的解决方案(不使用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'));

0

使用jQuery,您可以做到 -

function nodeInserted(elementQuerySelector){
    if ($(elementQuerySelector).length===0){
        setTimeout(function(){
            nodeInserted(elementQuerySelector);
        },100);
    }else{
        $(document).trigger("nodeInserted",[elementQuerySelector]);
    }
}

该函数递归搜索节点,直到找到它,然后针对文档触发事件。

然后您可以使用此方法来实现它。

nodeInserted("main");
$(document).on("nodeInserted",function(e,q){
    if (q === "main"){
        $("main").css("padding-left",0);
    }
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接