使函数等到元素存在

Posted

技术标签:

【中文标题】使函数等到元素存在【英文标题】:Make function wait until element exists 【发布时间】:2013-04-15 11:48:26 【问题描述】:

我正在尝试在另一个画布上添加一个画布——如何让这个函数等到第一个画布创建后才启动?

function PaintObject(brush) 

    this.started = false;

    // get handle of the main canvas, as a DOM object, not as a jQuery Object. Context is unfortunately not yet
    // available in jquery canvas wrapper object.
    var mainCanvas = $("#" + brush).get(0);

    // Check if everything is ok
    if (!mainCanvas) alert("canvas undefined, does not seem to be supported by your browser");
    if (!mainCanvas.getContext) alert('Error: canvas.getContext() undefined !');

    // Get the context for drawing in the canvas
    var mainContext = mainCanvas.getContext('2d');
    if (!mainContext) alert("could not get the context for the main canvas");

    this.getMainCanvas = function () 
        return mainCanvas;
    
    this.getMainContext = function () 
        return mainContext;
    

    // Prepare a second canvas on top of the previous one, kind of second "layer" that we will use
    // in order to draw elastic objects like a line, a rectangle or an ellipse we adjust using the mouse
    // and that follows mouse movements
    var frontCanvas = document.createElement('canvas');
    frontCanvas.id = 'canvasFront';
    // Add the temporary canvas as a second child of the mainCanvas parent.
    mainCanvas.parentNode.appendChild(frontCanvas);

    if (!frontCanvas) 
        alert("frontCanvas null");
    
    if (!frontCanvas.getContext) 
        alert('Error: no frontCanvas.getContext!');
    
    var frontContext = frontCanvas.getContext('2d');
    if (!frontContext) 
        alert("no TempContext null");
    

    this.getFrontCanvas = function () 
        return frontCanvas;
    
    this.getFrontContext = function () 
        return frontContext;
    

【问题讨论】:

当您在单击时创建画布时,运行该函数或触发一个事件来运行运行该函数的处理程序。当元素可用时不会发生内置的跨浏览器事件。 How to wait until an element exists? 的可能重复项 【参考方案1】:

如果您有权访问创建画布的代码 - 只需在创建画布后立即调用该函数。

如果您无权访问该代码(例如,如果它是第三方代码,例如谷歌地图),那么您可以做的就是在一个区间内测试是否存在:

var checkExist = setInterval(function() 
   if ($('#the-canvas').length) 
      console.log("Exists!");
      clearInterval(checkExist);
   
, 100); // check every 100ms

但请注意 - 很多时候,第 3 方代码在完成加载时可以选择激活您的代码(通过回调或事件触发)。那可能是您可以放置​​函数的地方。区间解决方案确实是一个糟糕的解决方案,只有在没有其他方法的情况下才应该使用。

【讨论】:

在 angularjs typeahead 中使用的完美解决方案。感谢您指引我正确的方向! 在将其他内容放入其中之前等待 Ajax 创建某些内容的绝佳解决方案。非常感谢。 @iftah 如果选择器是变量,我将如何让它工作?此外,如果它是 ID 或 Class 选择器也会更改。有时当我选择一个类时会返回多个元素,我需要找到一种方法将索引传递给选择器以确定哪个元素。我该怎么做?谢谢 @Kraglon 这是一个完全不同的问题,不适合这个答案的 cmets。我建议你问一个新问题,解释你尝试了什么,问题是什么,等等...... 在使用给定的解决方案时,还有一点很重要,您应该在 for 循环中包含该段代码并设置最大重试计数器,如果出现问题,您不会以无穷大结束循环:)【参考方案2】:

根据您需要支持的浏览器,可以选择MutationObserver。

编辑:所有主流浏览器support MutationObserver now。

类似的东西应该可以解决问题:

// callback executed when canvas was found
function handleCanvas(canvas)  ... 

// set up the mutation observer
var observer = new MutationObserver(function (mutations, me) 
  // `mutations` is an array of mutations that occurred
  // `me` is the MutationObserver instance
  var canvas = document.getElementById('my-canvas');
  if (canvas) 
    handleCanvas(canvas);
    me.disconnect(); // stop observing
    return;
  
);

// start observing
observer.observe(document, 
  childList: true,
  subtree: true
);

注意我自己没有测试过这段代码,但这是一般的想法。

您可以轻松地将其扩展为仅搜索已更改的 DOM 部分。为此,请使用 mutations 参数,它是 MutationRecord 对象的数组。

【讨论】:

喜欢这个。谢谢。 这种模式在很多情况下都非常有用,尤其是当你将 JS 拉入页面并且不知道是否加载了其他项目时。 最佳答案!谢谢! 我被旧浏览器 (ff38) 卡住了,这救了我。 这太棒了!我希望我早点知道这存在。【参考方案3】:

这仅适用于现代浏览器,但我发现使用 then 更容易,所以请先测试,但是:

ES5

function rafAsync() 
    return new Promise(resolve => 
        requestAnimationFrame(resolve); //faster than set time out
    );


function checkElement(selector) 
    if (document.querySelector(selector) === null) 
        return rafAsync().then(() => checkElement(selector));
     else 
        return Promise.resolve(true);
    

ES6

async function checkElement(selector) 
    const querySelector = null;
    while (querySelector === null) 
        await rafAsync();
        querySelector = document.querySelector(selector);
    
    return querySelector;
  

用法

checkElement('body') //use whichever selector you want
.then((element) => 
     console.info(element);
     //Do whatever you want now the element is there
);

【讨论】:

出现错误。使用生成器函数时,应在每个循环中更新 querySelector:while (document.querySelector(selector) === null) await rafAsync() 我能问一下你为什么要为选择器创建这么多变量赋值吗?这样做更好 afaik 的原因是它会更快,然后每次更改动画帧时都必须检查选择器。【参考方案4】:

一种更现代的等待元素的方法:

while(!document.querySelector(".my-selector")) 
  await new Promise(r => setTimeout(r, 500));

// now the element is loaded

请注意,此代码需要包装在 async function 中。

【讨论】:

这很漂亮! 那里的r 是什么? 嗯,好的,但它来自哪里?它有什么作用?你要寄什么给setTimeout @DanielMöller 您可能需要查看Promises 以更好地理解此代码。基本上,这里的代码所做的是设置 500 毫秒的超时,并等待它完成,然后再启动 while 循环的新迭代。聪明的解决方案! 这在 Chrome 控制台中运行循环代码时非常完美。由于不需要在此处使用通用等待 10 秒或在此处使用 30 秒来波动加载时间,它将我的运行时间减少到大约三分之一。另一个注意事项,如果您在 Chrome 控制台中运行循环代码,那么它不需要在异步函数中。您只需将上面的代码放在需要暂停的地方,直到元素出现。我不知道其他浏览器。我只将其更改为 getElementById 而不是一般的 querySelector。【参考方案5】:

这是对 Jamie Hutber 的回答的一个小改进

const checkElement = async selector => 
  while ( document.querySelector(selector) === null) 
    await new Promise( resolve =>  requestAnimationFrame(resolve) )
  
  return document.querySelector(selector); 
;

使用方法:

checkElement('.myElement').then((selector) => 
  console.log(selector);
);

【讨论】:

很好的答案。最少的代码行。这个答案不关注时钟并极大地减慢页面速度 - 相反,它使用requestAnimationFrame。现在编辑以展示如何使用它。 恕我直言,这比其他答案更好,并且使用 Promises,它比setInterval 性能更高。 +1【参考方案6】:

如果你想要一个使用 MutationObserver 的通用解决方案,你可以使用这个函数

// MIT Licensed
// Author: jwilson8767

/**
 * Waits for an element satisfying selector to exist, then resolves promise with the element.
 * Useful for resolving race conditions.
 *
 * @param selector
 * @returns Promise
 */
export function elementReady(selector) 
  return new Promise((resolve, reject) => 
    const el = document.querySelector(selector);
    if (el) resolve(el);
    new MutationObserver((mutationRecords, observer) => 
      // Query for elements matching the specified selector
      Array.from(document.querySelectorAll(selector)).forEach((element) => 
        resolve(element);
        //Once we have resolved we don't need the observer anymore.
        observer.disconnect();
      );
    )
      .observe(document.documentElement, 
        childList: true,
        subtree: true
      );
  );

来源:https://gist.github.com/jwilson8767/db379026efcbd932f64382db4b02853e 示例如何使用它

elementReady('#someWidget').then((someWidget)=>someWidget.remove(););

注意:MutationObserver 有很好的浏览器支持; https://caniuse.com/#feat=mutationobserver

等等! :)

【讨论】:

【参考方案7】:

requestAnimationFrame 中中继比在setTimeout 中中继更好。这是我在 es6 模块中使用 Promises 的解决方案。

es6、模块和承诺:

// onElementReady.js
const onElementReady = $element => (
  new Promise((resolve) => 
    const waitForElement = () => 
      if ($element) 
        resolve($element);
       else 
        window.requestAnimationFrame(waitForElement);
      
    ;
    waitForElement();
  )
);

export default onElementReady;

// in your app
import onElementReady from './onElementReady';

const $someElement = document.querySelector('.some-className');
onElementReady($someElement)
  .then(() => 
    // your element is ready
  

plain js and promises:

var onElementReady = function($element) 
  return new Promise((resolve) => 
    var waitForElement = function() 
      if ($element) 
        resolve($element);
       else 
        window.requestAnimationFrame(waitForElement);
      
    ;
    waitForElement();
  )
;

var $someElement = document.querySelector('.some-className');
onElementReady($someElement)
  .then(() => 
    // your element is ready
  );

【讨论】:

Uncaught TypeError: Cannot read property 'then' of undefined 我想我错过了回报......在新的承诺之前。 这是正确的解决方案,比所有基于 Timer 的定期检查要好得多。 实际上,这在当前形式下是行不通的。如果 $someElement 最初为 null(即 DOM 中尚不存在),那么您将这个 null 值(而不是 CSS 选择器)传递给您的 onElementReady 函数,并且该元素将永远不会被解析。相反,将 CSS 选择器作为文本传递,并尝试在每次传递时通过 .querySelector 获取对元素的引用。 @AndrásSzepesházi 解决方法很简单,传递一个函数而不是元素替换 if ($element)if (getElement()) 然后在开始时元素是否为空都没有关系解决方案的精神是一样的。这不会改变答案。【参考方案8】:

这是一个使用 observables 的解决方案。

waitForElementToAppear(elementId)                                           

    return Observable.create(function(observer)                             
            var el_ref;                                                      
            var f = () =>                                                   
                el_ref = document.getElementById(elementId);                 
                if (el_ref)                                                 
                    observer.next(el_ref);                                   
                    observer.complete();                                     
                    return;                                                  
                                                                            
                window.requestAnimationFrame(f);                             
            ;                                                               
            f();                                                             
        );                                                                  
                                                                            

现在你可以写了

waitForElementToAppear(elementId).subscribe(el_ref => doSomethingWith(el_ref);

【讨论】:

Observable 不是 React 组件吗?问题是关于 javascript 和 jQuery。 Observable 是 rxjs 的核心类型。见rxjs-dev.firebaseapp.com/guide/overview【参考方案9】:

您可以通过设置超时来检查 dom 是否已经存在,直到它已经在 dom 中呈现。

var panelMainWrapper = document.getElementById('panelMainWrapper');
setTimeout(function waitPanelMainWrapper() 
    if (document.body.contains(panelMainWrapper)) 
        $("#panelMainWrapper").html(data).fadeIn("fast");
     else 
        setTimeout(waitPanelMainWrapper, 10);
    
, 10);

【讨论】:

【参考方案10】:

Iftah 的另一种变体

var counter = 10;
var checkExist = setInterval(function() 
  console.log(counter);
  counter--
  if ($('#the-canvas').length || counter === 0) 
    console.log("by bye!");
    clearInterval(checkExist);
  
, 200);

以防万一元素从未显示,所以我们不会无限检查。

【讨论】:

【参考方案11】:

也许我有点晚了 :),但这是 chrisjhoughton 提供的一个不错且简短的解决方案,它允许在等待结束时执行回调函数。

https://gist.github.com/chrisjhoughton/7890303

var waitForEl = function(selector, callback) 
  if (jQuery(selector).length) 
    callback();
   else 
    setTimeout(function() 
      waitForEl(selector, callback);
    , 100);
  
;

waitForEl(selector, function() 
  // work the magic
);

如果需要给回调函数传参,可以这样使用:

waitForEl("#" + elDomId, () => callbackFunction(param1, param2));

但要小心!默认情况下,此解决方案可能会陷入无限循环的陷阱。

GitHub 线程中还提供了对 topicstarter 建议的一些改进。

享受吧!

【讨论】:

【参考方案12】:

只需将setTimeOut 与递归一起使用:

waitUntilElementIsPresent(callback: () => void): void 
    if (!this.methodToCheckIfElementIsPresent()) 
        setTimeout(() => this.waitUntilElementIsPresent(callback), 500);
        return;
    
    callback();

用法

this.waitUntilElementIsPresent(() => console.log('Element is present!'));

您可以限制尝试次数,因此当限制后元素不存在时会引发错误:

waitUntilElementIsPresent(callback: () => void, attempt: number = 0): void 
    const maxAttempts = 10;
    if (!this.methodToCheckIfElementIsPresent()) 
        attempt++;
        setTimeout(() => this.waitUntilElementIsPresent(callback, attempt), 500);
        return;
     else if (attempt >= maxAttempts) 
        return;
    
    callback();

【讨论】:

你在哪里定义你的元素?在元素出现之前,“this”还不存在。【参考方案13】:

这适用于那些在 Chrome 控制台中运行代码而不仅仅是硬编码到 html 中的人。

上面的 user993683 提供了可以在您的控制台代码中运行的代码。他/她的代码如下:

while(!document.querySelector(".my-selector")) 
  await new Promise(r => setTimeout(r, 500));

// now the element is loaded

他/她补充说“需要在异步函数中。”如果您在 Chrome 的控制台中使用代码,那么实际上您不要需要将其包装在一个函数中。它会像写的那样工作。您只需要在尝试访问该元素以确保它存在之前将其放置在您的代码中。

唯一需要注意的是,它不适用于仅在其他情况下有时才出现的元素。否则,如果元素从不下载,它将无限循环,您必须关闭浏览器才能停止等待。仅将其用于您确定会出现的元素。

我公司的表单页面有十几个或更多字段要为每个案例编号填写。而且我每天在脚本数组中都有数百个案例编号。更改 iFrame SRC 时元素不会同时加载,并且“onload”在 Chrome 控制台脚本中不起作用。所以这个方法对我来说是天赐之物,它每天至少为我节省 45 分钟,而不是由于加载时间波动而在这里等待 10 秒或那里等待 30 秒的旧通用异步等待。

我所做的唯一更改是“getElementById”而不是一般的“querySelector”,因为我需要的所有元素都有 ID。

while(!document.getElementById("myFrame").contentWindow.document.getElementById('someDocID')) 
      await new Promise(r => setTimeout(r, 500));
    
// After completing the wait above it is now safe to access the element
document.getElementById("myFrame").contentWindow.document.getElementById('someDocID'
).innerText = "Smith, John R";
// and now click the submit button then change the SRC to a fresh form, and use
//*emphasized text* the code again to wait for it to fully load

我向监视器道歉,但我添加了这个作为答案,因为经过几个月对控制台脚本的研究并等待元素加载,user993683 关于函数的评论终于让我意识到控制台脚本不需要函数这段代码。我的目标只是为其他控制台脚本用户保存与我相同的学习曲线。

【讨论】:

以上是关于使函数等到元素存在的主要内容,如果未能解决你的问题,请参考以下文章

如何等到 Selenium 中不再存在元素

如何等到硒中存在元素?

Python Selenium:等到元素不再陈旧?

Selenium - 等到元素存在、可见且可交互,即使在 C# 中的可滚动模式上也是如此

Selenium C# WebDriver:等到元素出现

jQuery和JS入口函数的区别