如何确定动态创建的DOM元素是否已添加到DOM中?

41

根据规范, 只有 BODYFRAMESET 元素提供了“onload”事件以进行附加,但我想知道在JavaScript中何时向DOM添加了动态创建的DOM元素。

我目前使用的超级幼稚的启发式方法如下:

  • 遍历元素的 parentNode 属性,直到找到最终祖先(即 parentNode.parentNode.parentNode.etc,直到 parentNode 为 null)

  • 如果最终祖先具有定义的、非空的 body 属性

    • 假设所讨论的元素是DOM的一部分
  • 否则

    • 在100毫秒后再次重复这些步骤

我想要的是确认我正在做的是否足够(同样,在IE7和FF3中都有效),或者一个更好的解决方案,出于某种原因,我完全没有意识到;也许还有其他应该检查的属性等。


编辑:我希望有一种跨浏览器的方法来实现这个,不幸的是,我生活在一个不止有一个浏览器的世界中;尽管如此,我们仍然欢迎浏览器特定的信息,但请注意你知道哪个浏览器它可以在其中工作。谢谢!


你怎么可能不知道文档中添加了什么内容?难道你不是这个页面的作者吗? - Sergey Ilinsky
2
@Sergey:你越是抽象事物,就越有可能出现一方不知道另一方在做什么的情况。 - eyelidlessness
没错 - 我正在构建一个“小部件”,它需要在内部知道何时被添加到DOM中 - 但由于我不一定是使用者,因此我不知道何时添加,因为客户端代码将决定。 - Jason Bunting
IMG的onload事件虽然不是W3C规范的一部分。此外,我希望这种功能适用于更多类型的元素,因此它对我没有太大的好处,而且还会变得更加脆弱。 - Jason Bunting
浏览器有一种方法可以确定一个节点是否在另一个节点内部:Node.contains。下面附上答案链接:https://dev59.com/FHVC5IYBdhLWcg3wqzHV#13725984 - Chris Calo
11个回答

30

更新:对于任何感兴趣的人,这是我最终使用的实现:

function isInDOMTree(node) {
   // If the farthest-back ancestor of our node has a "body"
   // property (that node would be the document itself), 
   // we assume it is in the page's DOM tree.
   return !!(findUltimateAncestor(node).body);
}
function findUltimateAncestor(node) {
   // Walk up the DOM tree until we are at the top (parentNode 
   // will return null at that point).
   // NOTE: this will return the same node that was passed in 
   // if it has no ancestors.
   var ancestor = node;
   while(ancestor.parentNode) {
      ancestor = ancestor.parentNode;
   }
   return ancestor;
}

我希望这样做是为了为DOM元素合成onload事件的一种方式。下面是该函数(尽管我正在使用与MochiKit结合使用的略微不同的东西):

function executeOnLoad(node, func) {
   // This function will check, every tenth of a second, to see if 
   // our element is a part of the DOM tree - as soon as we know 
   // that it is, we execute the provided function.
   if(isInDOMTree(node)) {
      func();
   } else {
      setTimeout(function() { executeOnLoad(node, func); }, 100);
   }
}

举个例子,这个设置可以按如下方式使用:

var mySpan = document.createElement("span");
mySpan.innerHTML = "Hello world!";
executeOnLoad(mySpan, function(node) { 
   alert('Added to DOM tree. ' + node.innerHTML);
});

// now, at some point later in code, this
// node would be appended to the document
document.body.appendChild(mySpan);

// sometime after this is executed, but no more than 100 ms after,
// the anonymous function I passed to executeOnLoad() would execute

希望对某些人有用。

注:我得出这个解决方案的原因是,与Darryl的回答相比,getElementById技术只在您在同一文档中时才有效; 我在页面上有一些iframe,并且页面以某种复杂的方式进行通信 - 当我尝试时,问题在于它无法找到该元素,因为它是不同于执行代码的文档的一部分。


我尝试了一下性能,遍历DOM树速度更快,只要元素不存在,http://jsperf.com/is-in-dom - hultqvist

22

最简单的答案是利用 Node.contains 方法,该方法被 Chrome、Firefox (Gecko)、Internet Explorer、Opera 和 Safari 支持。下面是一个示例:

var el = document.createElement("div");
console.log(document.body.contains(el)); // false
document.body.appendChild(el);
console.log(document.body.contains(el)); // true
document.body.removeChild(el);
console.log(document.body.contains(el)); // false

理想情况下,我们应该使用 document.contains(el),但这在 IE 中不起作用,所以我们使用 document.body.contains(el)

不幸的是,你仍然需要轮询,但检查元素是否已经存在于文档中非常简单:

setTimeout(function test() {
  if (document.body.contains(node)) {
    func();
  } else {
    setTimeout(test, 50);
  }
}, 50);

如果您可以在页面中添加一些CSS,这里有另一种聪明的技术,它使用动画来检测节点插入:http://www.backalleycoder.com/2012/04/25/i-want-a-damnodeinserted/


哦,在IE 9中,document.contains == undefined,但是document.body.contains != undefined。因此,并不一定是这种情况,这就是我的评论。 :) - Jason Bunting
你发的链接里的黑客攻击非常有用,在我的情况下起了很好的作用。 - gbaccetta
这会遍历整棵树吗?如果您添加了另一个节点作为第一个节点的子节点,然后调用document.body.contains(childNode),它会查找具有父节点在文档节点之前的childNode吗? - Rob Evans
1
@RobEvans,确实会的。 - Chris Calo
该方法无法检测 Edge(v42 +)和 Firefox(v62 +)中动态添加的节点。Chrome(v70)可以使用,但我还没有测试 Safari。我正在努力找出原因,但尚未发现任何线索。当然,在我的情况下,我实际上是检查 (parent) div.contains(child div) 而不是 document.contains,所以不确定是否应该有不同的行为。 - Mrchief
显示剩余2条评论

9

9

您可以执行document.getElementById('newElementId');,看看它是否返回true。如果没有,就像您说的那样,等待100毫秒,然后再尝试一次?


1
如果它没有ID呢?那么就不起作用了,这种情况可能会发生。除此之外,答案很好...也许我会看看它是否适用于我的某些需求,并在没有ID的情况下使用另一种方法。 - Jason Bunting
为什么不添加一个id呢?你可以使用某种随机字符串添加一个唯一的id,然后只需查找该id即可。我知道你不喜欢jQuery,但是使用jQuery,你可以很容易地根据类、父元素或子元素来查找它。 - Darryl Hein
是的,这可能是一个好方法 - 但我需要再想一想...谢谢。哈哈,我不喜欢jQuery。我确实喜欢它,只是不喜欢人们认为它是每个JavaScript问题的答案。 :) - Jason Bunting
1
所以,我也喜欢这个想法和Borgar的想法。我可以测试一下问题元素是否具有ID属性;如果是,则使用getElementById获取该属性,否则创建一个随机的、希望唯一的ID并分配它;当getElementById找到它时,删除最初不存在的ID并从那里继续前进。 - Jason Bunting
我喜欢这个想法。它很简洁。如果有什么东西可以工作,那么document.getElementById()几乎肯定会起作用。 :-) - Borgar
确实如此 - 简单、一致和跨浏览器的可靠性将使这成为更好的解决方案;我真的不想编写特定于浏览器的代码,因此我将更改批准的答案。 - Jason Bunting

4
MutationObserver 是用于检测元素何时被添加到 DOM 中的工具。 MutationObservers 已经被广泛支持,可以在所有现代浏览器中使用(Chrome 26+、Firefox 14+、IE11、Edge、Opera 15+ 等)。
以下是一个简单的示例,展示了如何使用 MutationObserver 监听元素何时被添加到 DOM 中。
为了简洁起见,我使用 jQuery 语法构建节点并将其插入到 DOM 中。
var myElement = $("<div>hello world</div>")[0];

var observer = new MutationObserver(function(mutations) {
   if (document.contains(myElement)) {
        console.log("It's in the DOM!");
        observer.disconnect();
    }
});

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

$("body").append(myElement); // console.log: It's in the DOM!
observer事件处理程序会在任何节点添加或从document中删除时触发。 在处理程序内部,我们执行contains检查以确定myElement现在是否在document中。
您不需要遍历存储在mutations中的每个MutationRecord,因为可以直接对myElement执行document.contains检查。
为了提高性能,请将document替换为DOM中包含myElement的特定元素。

2
你可以查询 document.getElementsByTagName("*").length 或者创建一个自定义的appendChild函数,如下所示:
var append = function(parent, child, onAppend) {
  parent.appendChild(child);
  if (onAppend) onAppend(child);
}

//inserts a div into body and adds the class "created" upon insertion
append(document.body, document.createElement("div"), function(el) {
  el.className = "created";
});

更新
应要求,将我的评论中的信息添加到我的帖子中。
今天在 Ajaxian 上,John Resig 发表了一篇关于 Peppy 库的评论,似乎暗示他的 Sizzle 库可以处理 DOM 插入事件。我很好奇是否会有代码来处理 IE。
继续使用轮询的想法,我读到一些元素属性直到将元素附加到文档中后才可用(例如 element.scrollTop),也许您可以轮询它而不是进行所有 DOM 遍历。
最后一件事:在 IE 中,一个值得探索的方法是玩弄 onpropertychange 事件。我认为将其附加到文档上肯定会触发至少一个属性的该事件。

正如我在直接出现在我的问题下面的评论中提到的那样,我可能无法控制对象何时添加到DOM中,否则我甚至不会问这个问题。但是感谢您的回复! - Jason Bunting
延续轮询的思路,我读到有些元素属性在元素附加到文档后才可用(例如element.scrollTop),也许你可以轮询它而不是进行所有DOM遍历。 - Leo
最后一件事:在IE中,一个值得探索的方法是尝试使用onpropertychange事件。我认为将其附加到文档上至少会触发一个属性的该事件。 - Leo
有趣的想法 - 我也可能会去了解一下,谢谢lhorie;等我有时间时,我会再深入研究一下,但现在至少我已经足够了;遍历和轮询确实很糟糕! - Jason Bunting
lhorie,您能否将这些想法添加到您的实际答案中(只需编辑即可)?我认为它们增加了价值,但由于它们是评论(可以被拥有足够声望的任何人轻松删除),因此它们的潜在用途不如其他内容。谢谢! - Jason Bunting
显示剩余3条评论

2
在一个完美的世界中,您可以挂钩突变事件。我怀疑它们甚至在标准浏览器上也不能可靠地工作。听起来您已经实现了一个突变事件,因此您可能可以添加此功能以使用本机突变事件而不是在支持这些事件的浏览器上进行超时轮询。
我曾看到过DOM更改的实现方式,通过比较document.body.innerHTML和最后保存的.innerHTML来监视更改,这并不是很优雅(但起作用)。由于您最终要检查是否已添加特定节点,因此最好在每个中断处检查一次。
我能想到的改进是使用.offsetParent而不是.parentNode,因为它可能会从循环中剔除一些父级元素(请参见注释)。或者在除了IE之外的所有浏览器上使用compareDocumentIndex()并测试IE的.sourceIndex属性(如果节点不在DOM中,则应为-1)。
这也可能有所帮助:John Resig的X浏览器compareDocumentIndex实现

原来offsetParent并不总是很好用...它并不总是能回到文档本身,而使用parentNode向上遍历树,如果节点是DOM树的一部分,则可以,否则最终返回null。并非所有通过offsetParent向上遍历树的路径都像这样工作。 - Jason Bunting
此外,offsetParent 显然不是 DOM 规范的一部分:https://developer.mozilla.org/En/DOM/Element.offsetParent 更重要的是,当元素的 style.display 设置为 'none' 时,它会返回 null,这会对我造成一些困扰。 - Jason Bunting

1

你需要使用 DOMNodeInserted 事件(或者 DOMNodeInsertedIntoDocument)。

编辑:IE 可能不支持这些事件,但我现在无法测试。


谢谢 - 我可以认为这是一个仅适用于Mozilla/Firefox/Gecko的习惯用语吗? - Jason Bunting
不,你没有这个权利。这是DOM事件的标准用语。 - Sergey Ilinsky
1
明白了。话虽如此,我认为IE(浏览器中的红发继子)不支持它。 - Jason Bunting

0
var finalElement=null; 

我正在循环中构建DOM对象,当循环处于最后一个周期并且创建了lastDomObject时:
 finalElement=lastDomObject; 

离开循环:

while (!finalElement) { } //this is the delay... 

下一个操作可以使用任何新创建的DOM对象。第一次尝试就成功了。

0

这里有另一个解决方案,提取自jQuery代码的问题。下面的函数可用于检查节点是否附加到DOM或是否“断开连接”。

function isDisconnected( node ) {
    return !node || !node.parentNode || node.parentNode.nodeType === 11;
}

nodeType === 11定义了DOCUMENT_FRAGMENT_NODE,它是一个未连接到文档对象的节点。

有关详细信息,请参见John Resig的以下文章: http://ejohn.org/blog/dom-documentfragments/


很抱歉,它的效果不如预期:var ol = document.createElement('ol'); var li = ol.appendChild(document.createElement('li')); ol.id = 'playtime'; isDisconnected(li); // 返回 false,但我希望它返回 true document.getElementById('playtime'); // 什么也没有返回 - soniiic

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