当前位置: 首页 > Web前端 > JavaScript

处理尚不存在的DOM节点

时间:2023-03-26 21:25:29 JavaScript

探索MutationObserverAPI与轮询和等待节点最终创建的传统方法相比的优缺点。有时您需要操作尚不存在的DOM部分。出现这种需求的原因有很多,但最常见的是在处理将标记异步注入页面的第三方脚本时。例如,我最近需要在用户关闭GooglereCAPTCHA挑战时更新UI。blur事件之类的响应是工具官方不支持的,所以我打算自己设计一个事件监听器。但是,尝试通过.querySelector()之类的方法访问节点返回null,因为此时节点还没有被浏览器渲染,不知道什么时候渲染。为了更深入地研究这一点,我设计了一个按钮,可以在随机时间(0到5秒之间)内安装在DOM中。如果我尝试从一开始就向按钮添加事件侦听器,则会出现异常。//模拟延迟渲染的HTML:setTimeout(()=>{constbutton=document.createElement('button');button.id='button';button.innerText='DoSomething!';document.body.append(button);},randomBetweenMs(1000,5000));document.querySelector('#button').addEventListener('click',()=>{alert('clicked!')});//错误:不能读取null的属性(读取“addEventListener”)真的不足为奇。你看到的所有代码都被扔到调用堆栈上并立即执行(当然,setTimeout回调除外),所以当我尝试访问按钮时,我得到的只是null。轮询为了解决这个问题,通常的做法是使用轮询,不断查询DOM,直到节点出现。您可能会看到使用了setInterval或setTimeout之类的方法,这是一个使用递归的示例:if(button){button.addEventListener('click',()=>alert('clicked!'));返回;}//如果该节点尚不存在,//在事件循环的下一回合再次尝试。setTimeout(attachListenerToButton);}attachListenerToButton();或者,您可能已经看到了一种基于Promise的方法,感觉更现代一些:asyncfunctionattachListenerToButton(){letbutton=document.getElementById('button');while(!button){//如果节点尚不存在,请在事件循环的下一回合再次尝试。按钮=文档。getElementById('按钮');等待新的承诺((解决)=>setTimeout(解决));}按钮。addEventListener('click',()=>alert('clicked!'));}attachListenerToButton();无论如何,这种策略的代价很大——主要是性能。在这两个版本中,删除setTimeout()会导致脚本完全同步运行,阻塞主线程以及其他需要在主线程上完成的任务。不会处理任何输入事件。您的标签将被冻结。混乱不会随之而来。在此处插入一个setTimeout()(或setInterval)以将下一次尝试推迟到事件循环的下一次迭代,以便可以同时执行其他任务。但是您仍然反复占用调用堆栈,等待您的节点出现。如果您希望您的代码很好地管理事件循环,这远非理想。您可以通过增加查询间隔(例如,每200ms查询一次)来减少调用堆栈的扩展。但是您冒着在节点出现和作业执行之间发生意外情况的风险。例如,如果您要添加一个点击事件侦听器,您不希望用户在几毫秒后附加该侦听器之前有机会单击该元素。此类问题可能很少见,但是当您稍后调试可能出错的代码时,它们肯定会引起烦恼。MutationObserver()MutationObserverAPI已经存在了一段时间,并在现代浏览器中得到广泛支持。它的作用很简单:当DOM树发生变化时做一些事情,包括插入节点时。但是作为一个原生的浏览器API,你不需要考虑像轮询这样的性能问题。观察体内任何变化的基本设置如下所示:constdomObserver=newMutationObserver((mutationList)=>{//document.body已更改!做点什么。});domObserver.observe(document.body,{childList:true,subtree:true});对于我们构建的示例,进一步细化相当简单。每当树发生变化时,我们将查询特定节点。如果该节点存在,则附加侦听器。constdomObserver=newMutationObserver(()=>{constbutton=document.getElementById('button');if(button){button.addEventListener('click',()=>alert('clicked!'));}});domObserver.observe(document.body,{childList:true,subtree:true});我们传递给.observe()的选项很重要。将childList设置为true会使观察者监视我们定位的节点(document.body)的更改,而subtree:true将导致监视其所有后代。不可否认,这里的API对我来说不是很容易理解,因此在根据自己的需要使用它之前花一些时间仔细考虑是值得的。无论如何,此特定配置最适合您不知道节点可能被注入何处的情况。但是,如果您确定它会出现在某个元素中,那么更精确地定位它会更明智。清理如果我们让观察者保持原样,则每个DOM更改都有可能向同一个按钮添加另一个单击事件侦听器。您可以通过将点击事件回调拉到MutationObserver回调之外的自己的变量中来解决此问题(.addEventListener()不会将侦听器添加到具有相同回调引用的节点),但在不再需要它时立即执行它会更直观地清理观察者。观察者有一个很好的方法可以做到这一点:constdomObserver=newMutationObserver((_mutationList,observer)=>{constbutton=document.getElementById('button');if(button){button.addEventListener('click',()=>console.log('clicked!'));//不需要再观察了。清理!observer.disconnect();}});响应性我之前提到过,轮询可能会在响应DOM更改时引入少量死时间。很多风险取决于您使用的间隔的大小,但是setTimeout()和setInterval()都在主任务队列上运行它们的回调,这意味着它们总是在事件循环的下一次迭代中运行。然而,MutationObserver在微任务队列上触发它的回调,这意味着它不需要等待事件循环的完整旋转来触发它的回调。它反应更快。我在浏览器中使用performance.now()做了一个基本实验,看看将点击事件侦听器添加到按钮需要多长时间,此时它被安装在DOM中。请记住,这是在我们的setTimeout()中没有设置延迟的情况下发生的,因此我们看到的延迟可能是事件循环本身的速度(加上其他因素)。以下是结果:方法添加侦听器的延迟轮询~8msMutationObserver()~.09ms这是一个非常显着的差异。使用轮询和零延迟setTimeout()附加侦听器比MutationObserver慢大约88倍。这很有效。总结考虑到性能优势、更简单的API和无处不在的浏览器支持,使用DOM轮询比MutationObserver更难获得优势。我希望您在处理自己项目中的惰性安装节点时发现它很有用。我自己也在寻找MutationObserver也可能有用的其他场景。以上就是本文的全部内容。如果对你有帮助,欢迎收藏、点赞、转发~