JavaScript:等待元素属性准备好后再继续执行

6

我有一个元素的属性,它是从第三方注入到我的页面中的:

document.querySelector('#embed-container #mf2-events').jsMF2

jsMF2是一个被注入的属性。我需要等待jsMF2属性被定义后才能使用它。最初,我只是设置了一个超时时间,但这样做有很多不良影响。是否有一种方法可以在属性被定义之前一直等待或设置回调函数?如果这是一个C程序,我会写出类似于这样的代码:

    while (document.querySelector('#embed-container #mf2-events').jsMF2 === undefined ||
           document.querySelector('#embed-container #mf2-events').jsMF2 === null) { } 

    // do work

你的意思是在多线程的C程序中这么做。JavaScript在浏览器中(除非被用作Web workers)确实是在单线程上运行的。 - T.J. Crowder
你只能使用setTimeout()进行轮询,或者在现代浏览器中使用MutationObserver。在while循环中旋转只会锁定浏览器,并且永远无法成功,因为没有其他代码可以运行以实际引起更改。 - jfriend00
@user3689167: 是的,但如果程序真正只有单线程,那么它将永远忙等待——因为没有任何东西有机会设置jsMF2。要在繁忙等待时做到这一点,您需要至少两个线程。 - T.J. Crowder
3个回答

5

你不希望使用繁忙等待,因为这会阻止其他JavaScript代码运行(更不用说浏览器UI的大部分),导致该属性无法被定义。

理想情况下,提供该属性的任何内容都应该有一个事件可以触发,您可以将其挂钩到其中。我假设您已经查找并未找到。

一旦ECMAScript6(又名“ES6”)中最新内容的支持变得普遍(目前尚未实现),您可能能够在此使用Proxy(如果您的目标浏览器允许在其HTML元素实例上使用Proxy)。但是,对于Proxy的广泛支持需要几年时间,如果不是更长时间(并且不能使用shim/polyfill来模拟Proxy)。 (在ES7中,您可以使用Object.observe,但是假定在ES7技术出现之前,由当前[截至2015年6月]标准定义的Proxy将得到广泛支持。)

在能够使用Proxy之前/之后,计时器确实是处理此情况的正确方法。如果需要,它可以是一个非常激进的计时器。

如果已知元素存在但正在等待属性:

check(function(element) {
    // It's there now, use it
    // x = element.jsMF2
});

function check(callback) {
    var element = document.querySelector('#embed-container #mf2-events');
    if (element && 'jsMF2' in element) {
        setTimeout(callback.bind(null, element), 0);
    } else {
        setTimeout(check.bind(null, callback), 0);
    }
}

大多数浏览器在javascript线程可用的前几次会立即触发计时器,然后将其限制为至少4ms的延迟以供后续调用。仍然相当快。

不过你不必过于激进;与计算机相比,人类很慢,你可能可以使用10、20或甚至50毫秒。

如果存在任何属性不会出现的可能性,最终你需要停止重复的setTimeout序列(一秒钟后,十秒钟后,三十秒钟后,六十秒钟后,适合你使用情况的时间)。你可以通过记住启动时间,并在等待时间太长时放弃而不是重新安排来实现这一点:

var started = Date.now();

check(function(element) {
    // It's there now, use it
    // x = element.jsMF2
});

function check(callback) {
    var element = document.querySelector('#embed-container #mf2-events');
    if (element && 'jsMF2' in element) {
        setTimeout(callback.bind(null, element), 0);
    } else {
        if (Date.now() - started > 1000) { // 1000ms = one second
            // Fail with message
        } else {
            setTimeout(check.bind(null, callback), 0);
        }
    }
}

侧记:查询语句
var document.querySelector('#embed-container #mf2-events');

这有点奇怪。它说:“给我第一个在具有id embed-container 的元素内找到的带有id mf2-events的元素。”但是在页面上,id必须是唯一的。因此,它实际上只是说:“获取#mfs-events元素,但仅当它在#embed-container元素内部时。”
除非这真的是你想要的,否则会更快。
var document.getElementById('mf2-events');

这是一个很好的选择。

请不要将setTimeout设置为0。如果不使用CPU,它会消耗电池电量。 - Dennis C
它确实如此。休眠时间越长,浪费的电池就越少。 - Dennis C
没有上下文,你不知道“运行代码”在做什么,直到第三方进行更改。这意味着它可能是一些网络负载、UI动画、CPU进程(任何你试图从中窃取CPU时间的进程)或用户交互。在大多数情况下,它只会浪费CPU资源。我使用setTimeout,0,但只用于“修复愚蠢的执行顺序”,以便我不再轮询。 - Dennis C
@DennisCheung:有时候轮询是你唯一的选择。如果明智地使用,只在必要时使用,那就没问题。 - T.J. Crowder
@user3689167:天啊!我漏掉了一个 return (但用 else 更好)。已修复。 - T.J. Crowder
显示剩余3条评论

0
唯一真正的解决方法是使用回调函数。属性是如何设置的?如果它是通过ajax请求设置的,您需要将一个函数作为请求参数添加进去,当请求满足时该函数将被执行。您可能需要了解回调函数、异步JavaScript和现代异步调用方法之一的Promise。

我不确定属性是如何设置的,它是通过我无法访问的第三方注入的。 - user3689167

-1

jsMF2 不是任何一个变异观察器可以观察的东西。它是一个扩展属性。变异观察器只能观察 DOM 的变化。 - T.J. Crowder
它是从document.querySelector返回的DOM元素。它可以被观察到。 - Dennis C
试一下。是的,element 是一个 DOM 元素。但这并不意味着 expando 属性是一个可以被观察到的 DOM 属性。想想看,你会告诉观察者观察什么?childList?它不是一个子节点。 attributesattributeOldValueattributeFilter?它不是一个属性。 characterDatacharacterDataOldValue?它不是字符数据。 subtree?再次强调,它不是一个节点。 - T.J. Crowder
甚至一些由DOM定义的反射属性也无法通过MutationObserver观察到,例如value(我们知道,它仅从value属性初始化):http://jsfiddle.net/L4e1kkfj/ - T.J. Crowder

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