这篇文章讨论了使用 Maps 和 WeakMaps 处理DOM节点的优势。Maps 和 WeakMaps 是非常实用的工具,尤其在处理大量DOM节点时,它们发挥着重要作用。
文章作者认为,使用 Maps 和 WeakMaps 处理 DOM 节点有以下几个优点。首先,它们可以方便地存储和检索数据。与其他数据结构相比,Maps和 WeakMaps 可以更简洁地组织和查找相关数据。其次,它们可以帮助开发者更好地管理内存。当不再需要某个DOM节点时,WeakMaps可以自动释放与该节点相关的内存,从而提高程序的性能。最后,使用 Maps 和 WeakMaps 可以提高代码的可读性和可维护性。将DOM节点与相关数据关联起来,有助于使代码更清晰易懂。
下面是正文:
在JAVAScript中, 我们经常使用普通的对象来存储键/值数据,它们非常擅长这项工作 - 清晰易读:
const person = {
firstName: 'Alex',
lastName: 'macArthur',
isACommunist: false
};
但是,当我们开始处理经常被读取、更改和添加属性的较大实体时,更倾向于使用 Maps。因为在某些情况下,Map 比对象具有多个优势,特别是性能问题或插入顺序比较重要的情况下。
但最近我特别喜欢使用它们来处理大量的DOM节点。
在阅读Caleb Porzio最近的博客文章时,我想到了这个想法。在这篇文章中,他正在使用由10,000个表行组成的表格,其中一个可以是“active”。为了管理选择不同行时的状态,使用对象作为键/值存储。这是他的一个迭代版本的注释版本。
import { ref, watchEffect } from 'vue';
let rowStates = {};
let activeRow;
document.querySelectorAll('tr').forEach((row) => {
// Set row state.
rowStates[row.id] = ref(false);
row.addEventListener('click', () => {
// Update row state.
if (activeRow) rowStates[activeRow].value = false;
activeRow = row.id;
rowStates[row.id].value = true;
});
watchEffect(() => {
// Read row state.
if (rowStates[row.id].value) {
row.classList.add('active');
} else {
row.classList.remove('active');
}
});
});
它使用一个对象作为大型哈希映射表,因此用于关联值的键必须是字符串,因此需要在每个项目上存在唯一的ID(或其他字符串值)。这带来了一些额外的编程开销,需要在需要时生成和读取这些值。
相反,使用 Map 可以让我们直接将 html 节点作为键。因此,该代码片段最终看起来像这样:
import { ref, watchEffect } from 'vue';
- let rowStates = {};
+ let rowStates = new Map();
let activeRow;
document.querySelectorAll('tr').forEach((row) => {
- rowStates[row.id] = ref(false);
+ rowStates.set(row, ref(false));
row.addEventListener('click', () => {
- if (activeRow) rowStates[activeRow].value = false;
+ if (activeRow) rowStates.get(activeRow).value = false;
activeRow = row;
- rowStates[row.id].value = true;
+ rowStates.get(activeRow).value = true;
});
watchEffect(() => {
- if (rowStates[row.id].value) {
+ if (rowStates.get(row).value) {
row.classList.add('active');
} else {
row.classList.remove('active');
}
});
});
这里最明显的好处是我不需要担心每行存在唯一ID。节点引用本身是唯一的,可以作为键。因此,既不需要设置也不需要读取任何属性。这更简单、更具弹性。
当我们处理更大的数据集时,操作的性能显著提高。甚至在规范中也有说明 - 必须以保持性能的方式构建 Map
,以便随着项目数量的增加而增长:
Maps must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection.
“Sublinear” 的意思是性能不会随着 Map 的大小成比例地下降。因此,即使是大型 Map,性能也应该保持相当迅速。
再次强调,没有必要干扰DOM属性或通过类似字符串的ID执行查找。每个键本身就是一个引用,这意味着我们可以跳过一两个步骤。
我进行了一些基本的性能测试来确认所有这些。首先,按照Caleb的场景,我在页面上生成了10,000个 <tr> 元素:
const table = document.createElement('table');
document.body.Append(table);
const count = 10_000;
for (let i = 0; i < count; i++) {
const item = document.createElement('tr');
item.id = i;
item.textContent = 'item';
table.append(item);
}
接下来,我设置了一个模板来测量循环遍历所有这些行并将一些相关状态存储在对象或Map
中需要多长时间。我还在 for
循环中运行了同样的过程多次,然后确定编写和读取所需的平均时间。
const rows = document.querySelectorAll('tr');
const times = [];
const testMap = new Map();
const testObj = {};
for (let i = 0; i < 1000; i++) {
const start = performance.now();
rows.forEach((row, index) => {
// Test Case #1
// testObj[row.id] = index;
// const result = testObj[row.id];
// Test Case #2
// testMap.set(row, index);
// const result = testMap.get(row);
});
times.push(performance.now() - start);
}
const average = times.reduce((acc, i) => acc + i, 0) / times.length;
console.log(average);
我用不同的行大小运行了这个测试:
请记住,即使是稍微不同的情况,这些结果也可能会有很大的差异,但总体而言,它们通常符合我的预期。在处理相对较少的项目时,Map 和对象之间的性能是可比的。但随着项目数量的增加,Map 开始拉开差距。性能的次线性变化开始显现。
有一个特殊版本的 Map 接口,旨在更好地管理内存 - WeakMap 。它通过保持对其键的“弱”引用来实现这一点,因此,如果这些对象键中的任何一个不再具有其他地方绑定的引用,则它有资格进行垃圾回收。因此,当不再需要该键时,整个条目将自动从 WeakMap 中删除,从而清除更多内存。它也适用于DOM节点。
为了调整这个,我们将使用 FinalizationRegistry ,它会在你正在观察的引用被垃圾回收时触发回调。我们将从几个列表项开始:
<ul>
<li id="item1">first</li>
<li id="item2">second</li>
<li id="item3">third</li>
</ul>
接下来,我们将把这些项目放入 WeakMap 中,并将 item2 注册为注册表监视的对象。我们将其删除,每当它被垃圾回收时,回调将被触发,我们将能够看到 WeakMap 如何发生变化。
但是...垃圾收集是不可预测的,也没有官方的方法来触发它,因此为了测试,我们将定期生成一堆对象并将它们保存在内存中。以下是整个脚本:
(async () => {
const listMap = new WeakMap();
// Stick each item in a WeakMap.
document.querySelectorAll('li').forEach((node) => {
listMap.set(node, node.id);
});
const registry = new FinalizationRegistry((heldValue) => {
// Garbage collection has happened!
console.log('After collection:', heldValue);
});
registry.register(document.getElementById('item2'), listMap);
console.log('Before collection:', listMap);
// Remove node, freeing up reference!
document.getElementById('item2').remove();
// Periodically create a bunch o' objects to trigger collection.
const objs = [];
while (true) {
for (let i = 0; i < 100; i++) {
objs.push(...new Array(100));
}
awAIt new Promise((resolve) => setTimeout(resolve, 10));
}
})();
在发生任何事情之前,WeakMap 如预期的那样包含三个项。但是在从DOM中删除第二项并进行垃圾收集之后,它看起来有点不同
由于节点引用在DOM中不再存在,整个条目已从 WeakMap 中删除,从而释放了更多的内存。这是一个很 nice 功能,有助于使环境的内存更加整洁。