处理尚不存在的 DOM 节点

处理,尚不,存在,dom,节点 · 浏览次数 : 170

小编点评

**MutationObserver API** * 使用者无需考虑性能问题,因为它在 DOM 树发生变化时自动执行操作。 * 观察器可以监视任何 DOM 树变化,包括插入、删除和更改节点。 * 观察器可以指定回调函数,当 DOM 树发生变化时执行。 **传统轮询等待最终被创建的节点方法的优劣** **优劣** | 方法 | 优劣 | |---|---| |轮询 | * 需要手动处理 DOM 树的变化 | | MutationObserver | * 性能更高 | |轮询 | * 容易发生错误 | **结论** MutationObserver API 是用于处理 DOM 树变化的性能优异的方法。它提供了无需手动处理 DOM 树变化的优势。但是,对于需要在 DOM 树发生变化时执行操作的代码,传统轮询可能是一个更好的选择。

正文

探索 MutationObserver API 与传统轮询等待最终被创建的节点方法相比的优劣。

有时候,您需要操作尚未存在的 DOM 的某个部分。

出现这种需求的原因有很多,但你最常看到的是在处理第三方脚本时,这些脚本会异步地将标记注入页面。举个例子,我最近需要在用户关闭Google reCAPTCHA的挑战时更新UI。诸如blur事件的响应并没有得到工具的正式支持,所以我打算自己来设计一个事件监听器。然而,通过像.querySelector()这样的方法来尝试访问节点会返回null,因为此时节点还没有被浏览器渲染,并且我也不知道究竟什么时候会被渲染。

为了更深入地探讨这个问题,我设计了一个按钮,让它在随机的时间内(0到5秒之间)被挂载到DOM中。如果我试图从一开始就给这个按钮添加一个事件监听器,我就会得到一个异常。

// Simulating lazily-rendered HTML:
setTimeout(() => {
	const button = document.createElement('button');
	button.id = 'button';
	button.innerText = 'Do Something!';

 	document.body.append(button);
}, randomBetweenMs(1000, 5000));

document.querySelector('#button').addEventListener('click', () => {
	alert('clicked!')
});

// Error: Cannot read properties of null (reading 'addEventListener')

真的是毫无意外。你看到的所有代码都会被丢进调用栈并立即执行(当然,除了setTimeout的回调函数),所以当我试图访问按钮时,我所得到的便是null

轮询

为了解决这个问题,通常做法是使用轮询,不停的查询DOM直到节点出现。你可能会看到使用setInterval或者setTimeout这样的方法,下面是使用递归的例子:

function attachListenerToButton() {
  let button = document.getElementById('button');

  if (button) {
    button.addEventListener('click', () => alert('clicked!'));
    return;
  }

	// If the node doesn't exist yet, try
	// again on the next turn of the event loop.
  setTimeout(attachListenerToButton);
}

attachListenerToButton();

或者,你可能已经见过一种基于Promise的方法,这感觉更现代一些:

async function attachListenerToButton() {
  let button = document.getElementById('button');

  while (!button) {
		// If the node doesn't exist yet, try
		// again on the next turn of the event loop.
    button = document.getElementById('button');
    await new Promise((resolve) => setTimeout(resolve));
  }

  button.addEventListener('click', () => alert('clicked!'));
}

attachListenerToButton();

不管怎么说,这种策略都有非同小可的代价--主要是性能。在这两个版本中,移除setTimeout()会导致脚本完全同步运行,阻塞主线程,以及其他需要在主线程上进行的任务。没有输入事件会被处理。你的标签会被冻结。混乱不会随之而来。

在这里插入一个setTimeout()(或者setInterval),将下一次尝试推迟到到事件循环的下一个迭代中,这样就可以在这期间执行其他任务。但你仍然在重复地占用调用栈,等待你的节点出现。如果你想让你的代码很好地管理事件循环,那这就太不理想了。

你可以通过增加查询的间隔时间(比如每200ms查询一次)来减少调用栈的膨胀。但是你会面临这样的风险,即在节点出现和你的工作执行之间发生了意想不到的事情。例如,如果你正在添加一个click事件监听器,你不希望用户在几毫秒后才附加监听器之前就有机会点击该元素。这样的问题可能很少见,但当你稍后调试可能出错的代码时,它们肯定会带来烦恼。

MutationObserver()

MutationObserver API 已经存在一段时间了,在现代浏览器中得到了广泛支持。它的作用很简单:当 DOM 树发生变化(包括插入节点时)时执行某些操作。但是作为原生浏览器 API,你不需要像轮询一样考虑性能问题。观察 body 内部任何变化的基本设置如下所示:

const domObserver = new MutationObserver((mutationList) => {
	// document.body has changed! Do something.
});

domObserver.observe(document.body, { childList: true, subtree: true });

对于我们构造的示例,进一步完善也相当简单。每当树发生变化时,我们将查询特定的节点。如果节点存在,则附加监听器。

const domObserver = new MutationObserver(() => {
  const button = 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() 不会向具有相同回调引用的节点添加监听器),但在不再需要它时即时清理观察器会更加直观。观察器上有一个很好的方法可以做到这一点:

const domObserver = new MutationObserver((_mutationList, observer) => {
	const button = document.getElementById('button');

	if (button) {
    	button.addEventListener('click', () => console.log('clicked!'));

		// No need to observe anymore. Clean up!
		observer.disconnect();
 	}
});

响应速度

我之前提到了轮询可能会在响应 DOM 更改时引入少量的假死时间。很多风险取决于你使用的时间间隔大小,但 setTimeout()setInterval() 都在主任务队列上运行它们的回调,这意味着它们总是在事件循环的下一次迭代中运行。

然而,MutationObserver 在微任务队列上触发其回调,这意味着它不需要等待事件循环的完整旋转就可以触发回调。它的响应性更高。

我在浏览器中使用 performance.now() 进行了一项基础实验,以查看将点击事件监听器添加到按钮上需要多长时间,此时它已挂载到 DOM 中。请记住,这是在我们的 setTimeout() 中没有设置延迟的情况下进行的,因此我们看到的延迟可能是事件循环本身的速度(加上其他因素)。以下是结果:

方法 添加监听器的延迟
轮询 ~8ms
MutationObserver() ~.09ms

这是一个非常惊人的差异。使用轮询和零延迟的 setTimeout() 来附加监听器的速度,大约比 MutationObserver 慢了 88 倍。这效果还不错。

总结

考虑到性能优势、更简单的 API 和普遍的浏览器支持,与 MutationObserver 相比,使用 DOM 轮询难以获得优势。我希望你在处理自己项目中的延迟挂载节点时会发现它很有用。我自己也会寻找其他场景,在这些场景下,MutationObserver 可能也很有用。

以上就是本文的全部内容,如果对你有所帮助,欢迎收藏、点赞、转发~

与处理尚不存在的 DOM 节点相似的内容:

处理尚不存在的 DOM 节点

探索 MutationObserver API 与传统轮询等待最终被创建的节点方法相比的优劣。 有时候,您需要操作尚未存在的 DOM 的某个部分。 出现这种需求的原因有很多,但你最常看到的是在处理第三方脚本时,这些脚本会异步地将标记注入页面。举个例子,我最近需要在用户关闭Google reCAPTC

刺激!ChatGPT给我虚构了一本书?

ChatGPT很强大,可以帮我们处理很多问题,但这些问题的答案的正确性您是否有考证过呢? 昨晚,DD就收到了一个有趣的反馈: 提问:有什么关于数据权限设计的资料推荐吗? ChatGPT居然介绍了一本根本不存在的书《数据权限设计与实现》,作者居然还是我... 那么你在使用ChatGPT的时候,有碰到过

MySQL innoDB 间隙锁产生的死锁问题

线上经常偶发死锁问题,当时处理一张表,也没有联表处理,但是有两个mq入口,并且消息体存在一样的情况,频率还不是很低,这么一个背景,我非常容易怀疑到,两个消息同时近到这一个事务里面导致的,但是是偶发的,又模拟不出来什么场景会导致死锁,只能进行代码分析,问题还原的方式去排查问题。

【pandas小技巧】--缺失值的列

在实际应用中,数据集中经常会存在缺失值,也就是某些数据项的值并未填充或者填充不完整。缺失值的存在可能会对后续的数据分析和建模产生影响,因此需要进行处理。 `pandas`提供了多种方法来处理缺失值,例如删除缺失值、填充缺失值等。删除缺失值可能会导致数据量减少,填充缺失值则能够尽量保留原始数据集的完整

Stirling-PDF 安装和使用教程

PDF (便携式文档格式) 目前已经成为了文档交换和存储的标准。然而,找到一个功能全面、安全可靠、且完全本地化的 PDF 处理工具并不容易。很多在线 PDF 工具存在隐私和安全风险,而桌面软件往往价格昂贵或功能有限。那么,有没有一种解决方案能够兼顾功能强大、安全可靠和经济实惠呢? 今天给大家推荐一款

从0到1构建基于自身业务的前端工具库

在实际项目开发中无论 M 端、PC 端,或多或少都有一个 utils 文件目录去管理项目中用到的一些常用的工具方法,比如:时间处理、价格处理、解析url参数、加载脚本等,其中很多是重复、基础、或基于某种业务场景的工具,存在项目间冗余的痛点以及工具方法规范不统一的问题

Python处理Oracle数据库的学习过程

Python处理Oracle数据库的学习过程 背景 产品数据存在一些大小写敏感的数据迁移到不敏感的数据库时出现报错的情况. 基于此, 我这边跟帅男同学学习了下Python的使用. 因为这一块一直比较菜.所以想着进行一下总计和备忘. 感谢帅男提供的支持与帮助. 环境安装 https://downloa

.NET性能优化-使用RecyclableMemoryStream替代MemoryStream

提到MemoryStream大家可能都不陌生,在编写代码中或多或少有使用过;比如Json序列化反序列化、导出PDF/Excel/Word、进行图片或者文字处理等场景。但是如果使用它高频、大数据量处理这些数据,就存在一些性能陷阱。 今天给大家带来的这个优化技巧其实就是池化MemoryStream的版本

[转帖]003、体系结构之TiKV持久化

TiKV架构和作用 数据持久化分布式一致性MVCC分布式事务Coprocessor coprocessor : 协同处理器。 可以将一些SQL计算交给TiKV处理。不需要将TiKV所有数据通过网络发送给TiDB Server RocksDB 任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV 也

[apue] 一图读懂 Unix 时间日期例程相互关系

GMT 和 UTC 时间有何区别?Unix 时间例程为何不处理闰秒?系统时区是如何设置的?哪些时间例程受夏时制影响?localtime 和 gmtime 是否共享内部存储区?strftime 获取第几周使用的 %U/%V/%W 有何区别?linux date 和 mac date 语法有何区别?本文一一为你解答