4

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

 1 year ago
source link: https://www.fly63.com/article/detial/12392
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

更新日期: 2023-02-28阅读: 28标签: 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 的性能测试对比:

63fd52972584e.jpg

当然,本文只是给了一个简单的示例,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://www.fly63.com/article/detial/12392


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK