探索 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 节点的更多相关文章

  1. 深入理解DOM节点操作

    × 目录 [1]创建节点 [2]插入节点 [3]移除节点[4]替换节点[5]复制节点 前面的话 一般地,提起操作会想到“增删改查”这四个字,而DOM节点操作也类似地对应于此,接下来将详细介绍DOM的节 ...

  2. 深入理解DOM节点关系

    × 目录 [1]父级属性 [2]子级属性 [3]同级属性[4]包含方法[5]关系方法 前面的话 DOM可以将任何HTML描绘成一个由多层节点构成的结构.节点分为12种不同类型,每种类型分别表示文档中不 ...

  3. 深入理解DOM节点类型第五篇——元素节点Element

    × 目录 [1]特征 [2]子节点 [3]特性操作[4]attributes 前面的话 元素节点Element非常常用,是DOM文档树的主要节点:元素节点是html标签元素的DOM化结果.元素节点主要 ...

  4. 深入理解DOM节点类型第六篇——特性节点Attribute

    × 目录 [1]特征 [2]属性 [3]方法 前面的话 元素的特性在DOM中以Attr类型表示,从技术角度讲,特性是存在于元素的attributes属性中的节点.尽管特性是节点,但却不是DOM节点树的 ...

  5. 深入理解DOM节点类型第四篇——文档片段节点DocumentFragment

    × 目录 [1]特征 [2]作用 前面的话 在所有节点类型中,只有文档片段节点DocumentFragment在文档中没有对应的标记.DOM规定文档片段(document fragment)是一种“轻 ...

  6. 深入理解DOM节点类型第一篇——12种DOM节点类型概述

    × 目录 [1]元素 [2]特性 [3]文本[4]CDATA[5]实体引用[6]实体名称[7]处理指令[8]注释[9]文档[10]文档类型[11]文档片段[12]DTD 前面的话 DOM是javasc ...

  7. DOM 节点的克隆与删除

    无奈的开头 关于DOM节点操作,如果仅仅是根据标准API来操作,那是最简单不过的了.但是现实中却哪有这么容易的问题让我们解决,其实不仅仅是节点的克隆与删除,节点的添加也是如此,而且添加节点需要考虑的情 ...

  8. DOM节点属性

    节点属性 在文档对象模型 (DOM) 中,每个节点都是一个对象.DOM 节点有三个重要的属性 : 1. nodeName : 节点的名称 2. nodeValue :节点的值 3. nodeType ...

  9. 第6章 DOM节点操作

    一.创建节点 为了使页面更加智能化,有时我们想动态的在 html 结构页面添加一个元素标签,那么 在插入之前首先要做的动作就是:创建节点. varbox=$('<div id="box ...

  10. dom节点的操作

    dom节点的操作 -- jQuery 内部插入 1.(结尾)append 方法 . appendto方法(为了方便链式操作) (开头)prepend方法           $('#div1').ap ...

随机推荐

  1. mac中Mysql各种版本下载

    https://downloads.mysql.com/archives/community/ 安装后,5.8以下修改密码,参考 https://www.cnblogs.com/xuyin/p/121 ...

  2. git和coding的使用

    1.注册 coding ::::::https://coding.net/ 2.个人设置中添加上邮箱账号和密码 3.下载git 4.在coding中新建项目,并对项目初始化---生成分支,会生成url ...

  3. pycharm 安装步骤

    1.双击安装包 2.点击next 3.选择安装目录后点击next进入下一步 4.根据你电脑的实际情况选择安装32位还是64位 5.勾选 .py 后即可点击next进入下一步 6.默认,直接next 7 ...

  4. 解决vuex“状态管理调用报错”报错为:"Uncaught ReferenceError: mapactions is not defined"

    报错: 源码: <script> import Vuex from 'vuex'; import {mapActions,mapGetters} from 'vuex'; // conso ...

  5. Git系列 -> 如何获取远程库某个文件

    有时候远程库存放很多文件夹,我们可能只需要下载或获取某个文件中的内容,可以采用以下: 首先点击clone 按钮,然后复制clone URL , 如果想要下载其中的某个文件夹的内容,需要在后面加入指定文 ...

  6. ImmutablePair和ImmutableTriple的使用

    场景 当我们调用某个方法,需要有超过一个值的返回时,我们通常会怎么做,比如现在需要调用方法返回姓名和年龄两个属性(假如姓名为张三,年龄为12). 方式一:使用Map,将Map中分别放入name为key ...

  7. 将Vue项目部署到Nginx中,出现的400,405,200响应空等问题处理

    最近用Vue3写了个项目,然后对接后台接口. 在本地vue配置文件中,配置了反向代理.成功请求了后端接口. 自测没有问题. 打包vue,发布到nginx中.运行nginx,成功显示了页面. 当点击页面 ...

  8. Python在字典中通过键名查找键值

    def find(target, dict_data): """ :param target: 需要查找的键名 :param dict_data: 需要查找的列表 :re ...

  9. EXCEL之VLOOKUP函数——查找、分组、排序

    1,VLOOKUP查找 新建excel,输入测试数据,F列打乱A顺序,查找他们各自对应的学号 选中目标单元格(此处是G2),点击插入函数: 选择VLOOKUP: 我们要查找F列在A列对应的学号,所以G ...

  10. VueUse实用工具

    1.安装 npm i @vueuse/core 2.使用 useClipboard 剪贴板 <script setup lang="ts"> import { ref ...