如何更好的处理还未创建的 DOM 节点?

更新日期: 2023-02-28 阅读: 2.2k 标签: dom 作者: ConardLi

大家好,我是 ConardLi。

在平时的研发需求中,我们可能会遇到这样的场景:某些 dom 节点还没创建出来,我们也不知道它何时创建出来。但是我们希望在这个节点创建出来的时候做点啥事情,比如创建一个事件监听器。

这种需求在处理某些三方的 JavaScript 脚本的时候特别常见,因为这些元素的创建是我们不可控的,下面举一个实际的例子:

我们想调用 Google 提供的 reCAPTCHA 能力来验证用户是否为机器人,然后我们想在用户点击了 reCAPTCHA 之后来更新 UI,但是 reCAPTCHA 本身也没有提供像 blur 这样的事件,我们可能需要自己来为它添加一个事件监听器,但我们又不知道这个元素啥时候会被创建出来。

下面我们来模拟一个简单的例子,我们用一个定时器来模拟一个 DOM 元素的随机创建,设定一个 0-17 秒的范围。

setTimeout(() => {

const input = document.createElement('input');
input.id = 'robot-check';
input.placeholder = '验证你不是个机器人!';
document.body.append(input);

}, Math.random() * 17 * 1000);

如果我们一开始就直接为这个元素创建一个事件监听器,那肯定会报错:

document.querySelector('#robot-check').addEventListener('blur', () => {
alert('有好事即将发生!')
});

为了解决这个问题,我们通常会使用轮询的方式:通过 setTimeout 或者 setInterval 每隔一段时间查询一下 DOM 节点有没有被创建出来:

function attachListener() {
let input = document.getElementById('robot-check');

if (input) {
input.addEventListener('blur', () => alert('有好事即将发生!'));
return;
}

// 递归调用实现轮询
setTimeout(attachListener, 100);
}

attachListener();

或者基于 Promise,你可以这样优化一下你的代码

async function attachListener() {
let input = document.getElementById('robot-check');

while (!input) {
input = document.getElementById('robot-check');
await new Promise((resolve) => setTimeout(resolve, 100));
}

input.addEventListener('blur', () => alert('有好事即将发生!'));
}

attachListener();

不论以上哪种实现方式,都会带来不小的性能成本,比如如果我们删除了 setTimeout ,会直接导致脚本完全同步运行,从而阻塞主线程以及需要在主线程上执行的任何其他任务。setTimeout 会不断把事件追加到下一次事件循环中,这会让我们的调用堆栈不断膨胀。所以我们可能会调大轮询的时间间隔,但是这又会损失一部分用户体验,也不是一个很好的选择。

下面就有请我们的 MutationObserver api 闪亮登场了。

MutationObserver API 就是为观测 DOM 而生,它的目的非常明确:当 DOM 树发生变化的时候(包括插入、删除、更新节点)做一些事情。比如下面的代码可以让你观测到 Body 内部的任何变化(当 DOM 发生变化时自动执行回调函数):

const domObserver = new MutationObserver((mutationList) => {
// body 结构发生了变化,可以做点事情~
});

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

作为原生浏览器 API,我们不用担心会有像使用轮询那样的性能问题。再回到我们前面的示例,可以这样实现:

const domObserver = new MutationObserver(() => {

const input = document.getElementById('robot-check');

if (input) {
input.addEventListener('click', () => alert('有好事即将发生!'));
}
});

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

observe() 方法接受两个参数,第一个参数是我们要观测的 DOM 节点,第二个参数还有一些可选配置:

  • childList:一个布尔值,表示是否观察目标节点的子节点的添加或删除。
  • attributes:一个布尔值,表示是否观察目标节点属性的变化。
  • characterData:一个布尔值,表示是否观察目标节点文本内容的变化。
  • subtree:一个布尔值,表示是否观察目标节点的所有后代节点的变化。
  • attributeOldValue:一个布尔值,表示观察属性变化时是否在 MutationRecord 对象中包含变化前的属性值。
  • characterDataOldValue:一个布尔值,表示观察文本内容变化时是否在 MutationRecord 对象中包含变化前的文本内容。
  • attributeFilter:一个数组,包含要观察的属性名,只有在属性变化时才会触发回调函数。

另外还有比较重要的一点,当我们做完需要处理的事情之后,做一下清理,不然就可能重复执行,我们可以在观测结束后调用 observer.disconnect(); 方法:

const domObserver = new MutationObserver(() => {

const input = document.getElementById('robot-check');

if (input) {
input.addEventListener('click', () => alert('有好事即将发生!'));

observer.disconnect();
}
});

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

MutationObserver API 目前已经得到所有主流浏览器的广泛支持,所以可以放心使用~

我们前面提到,setTimeout、setInterval 性能取决于我们怎么设定轮询的时间间隔,它们都会在主任务队列上运行它们的回调函数。但是,MutationObserver 会在微任务队列上触发它的回调,这意味着它无需等待事件循环的完整过程即可触发回调,所以它的响应速度要快得多。

下面是使用 setTimeout(间隔设定为 0ms) 和 MutationObserver 的性能测试对比:


当然,本文只是给了一个简单的示例,MutationObserver 还可以在更多的场景发挥更大的作用,比如 DOM 的防窜改等等

参考链接

  • https://www.google.com/recaptcha/about/
  • https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
  • https://www.macarthur.me/posts/use-mutation-observer-to-handle-nodes-that-dont-exist-yet
原文:code秘密花园

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/12392

相关推荐

全面理解虚拟DOM,实现虚拟DOM

DOM是很慢的,其元素非常庞大,页面的性能问题鲜有由JS引起的,大部分都是由DOM操作引起的。虚拟的DOM的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地DOM操作。

js实现DOM遍历_遍历dom树节点方法

遍历DOM节点常用一般用节点的 childNodes, firstChild, lastChild, nodeType, nodeName, nodeValue属性。在获取节点nodeValue时要注意,元素节点的子文本节点的nodeValue才是元素节点中文本的内容。

vue中使用ref获取dom对象

本文适用于所有使用 Vue 的开发人, 包括初学者。在阅读本文之前,你应该具备一些前提条件:Node.js 10.x或更高版本。在终端或命令提示符下运行 node -v 来验证你的版本;npm 6.7 或以上版本;代码编辑器;我强烈推荐 Visual Studio Code

getHTML() - 替代 innerHTML 的最佳方法

getHTML()为开发者提供了一种强大的方法来处理包含Shadow DOM的复杂DOM结构。虽然它有一些限制,但在处理现代Web组件和复杂UI时,getHTML()的优势是显而易见的。随着Web组件的普及,掌握getHTML()将成为前端开发者的重要技能。

vuejs2.0如何获取dom元素自定义属性值

设置定义属性值 :data-value=.., 2.直接获取 3.通过this.$refs.***获取 1.目标DOM定义ref值: 2.通过 【this.$refs.***.属性名】 获取相关属性的值: this.$refs.*** 获取到对应的元素 ...

HTML文档解析和DOM树的构建

浏览器解析HTML文档生成DOM树的过程,以下是一段HTML代码,以此为例来分析解析HTML文档的原理.解析HTML文档构建DOM树的理解过程可分为两个主要模块构成,即标签解析、DOM树构建

原生js获取DOM对象的几种方法

javascript获取DOM对象的多种方法:通过id获取 、通过class获取、通过标签名获取、通过name属性获取、通过querySelector获取、通过querySelectorAll获取等

关于DOM操作是异步的还是同步的相关理解

先列出我的理解,然后再从具体的例子中说明:DOM操作本身应该是同步的(当然,我说的是单纯的DOM操作,不考虑ajax请求后渲染等);DOM操作之后导致的渲染等是异步的(在DOM操作简单的情况下,是难以察觉的)

详解 HTML attribute 和 DOM property

在大多数的文章中,attribute 一般被翻译为“特性”,property 被译为“属性”。把结论写在最前面,如果你全都懂,后面就不用看了。当我们书写 HTML 代码的时候

JavaScript DOM事件模型

早期由于浏览器厂商对于浏览器市场的争夺,各家浏览器厂商对同一功能的JavaScript的实现都不进相同,本节内容介绍JavaScript的DOM事件模型及事件处理程序的分类。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!