我希望在将 DOM 元素添加到页面时运行我选择的功能。这是在浏览器扩展的上下文中,因此网页独立于我运行,我无法修改其源代码。我在这里有什么选择?
我想,从理论上讲,我可以用它setInterval()
来不断搜索元素的存在并在元素存在时执行我的操作,但我需要一个更好的方法。
我希望在将 DOM 元素添加到页面时运行我选择的功能。这是在浏览器扩展的上下文中,因此网页独立于我运行,我无法修改其源代码。我在这里有什么选择?
我想,从理论上讲,我可以用它setInterval()
来不断搜索元素的存在并在元素存在时执行我的操作,但我需要一个更好的方法。
警告!
这个答案现在已经过时了。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(十分之一)秒。除非您需要实时元素观察,否则应该足够了。
实际答案是“使用突变观察者”(如本问题所述:Determining if a HTML element has been added to the DOM dynamic),但是支持(特别是在 IE 上)是有限的(http://caniuse.com/mutationobserver) .
所以实际的实际答案是“使用突变观察者......最终。但现在选择 Jose Faeti 的答案”:)
在突变事件的弃用和突变事件的出现之间MutationObserver
,当特定元素添加到 DOM 时得到通知的一种有效方法是利用 CSS3 动画事件。
引用博客文章:
设置一个 CSS 关键帧序列,以(通过您选择的 CSS 选择器)您希望接收 DOM 节点插入事件的任何 DOM 元素为目标。
我使用了一个相对温和且很少使用的 css 属性,clip我使用了轮廓颜色以避免与预期的页面样式混淆 - 代码曾经针对 clip 属性,但从版本 11 开始,它在 IE 中不再是可动画的。说,任何可以动画的属性都会起作用,选择你喜欢的任何一个。接下来,我添加了一个文档范围的 animationstart 侦听器,用作处理节点插入的委托。动画事件上有一个名为 animationName 的属性,它告诉您哪个关键帧序列启动了动画。只需确保 animationName 属性与您为节点插入添加的关键帧序列名称相同即可。
ETA 24 Apr 17async
我想用一些/魔法
来简化一下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)
}
}
原来的
你可以在没有库的情况下做到这一点,但你必须使用一些 ES6 的东西,所以要注意兼容性问题(即,如果你的受众主要是 Amish、luddite 或者更糟糕的是 IE8 用户)
首先,我们将使用MutationObserver API来构造一个观察者对象。我们将把这个对象包装在一个 Promise 中,并resolve()
在触发回调时 (h/t 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);
})
}
然后,我们将创建一个generator function
. 如果你还没有使用这些,那么你就错过了——但一个简短的概要是:它像一个同步函数一样运行,当它找到一个yield <Promise>
表达式时,它会以非阻塞的方式等待 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")
}
生成器的一个棘手部分是它们不像正常函数那样“返回”。因此,我们将使用辅助函数来像使用常规函数一样使用生成器。(再次,h/t 到 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 突变集成到同步样式的控制流中。那怎么办。
(见底部重新审视的答案)
您可以使用livequery
jQuery 插件。您可以提供选择器表达式,例如:
$("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
.
看看这个插件确实做到了 - jquery.initialize
它的工作原理与 .each 函数完全一样,不同之处在于它需要您输入的选择器并观察将来添加的新项目匹配此选择器并初始化它们
初始化看起来像这样
$(".some-element").initialize( function(){
$(this).css("color", "blue");
});
但是现在如果页面上出现新的元素匹配.some-element
选择器,它将被立即初始化。
添加新项目的方式并不重要,您不需要关心任何回调等。
因此,如果您要添加新元素,例如:
$("<div/>").addClass('some-element').appendTo("body"); //new element will have blue color!
它将立即初始化。
插件基于MutationObserver
使用 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);
}
});
纯 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'));