本文浅析一下为什么Map
(和 WeakMap)在解决大量 DOM 节点时特地有用。
咱们在 JavaScript 中应用了很多一般的、古老的对象来存储键 / 值数据,它们解决的十分杰出:
const person = {
firstName: 'Alex',
lastName: 'MacArthur',
isACommunist: false
};
然而,当你开始解决较大的实体,其属性常常被读取、更改和增加时,人们越来越多地应用 Map
来代替。这是有起因的:在某些状况下,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(或其余字符串值)。这带来了一些额定的程序性开销,以便在须要时生成和读取这些值。
对象即 key
与之对应的是,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
的构建形式必须可能在我的项目数量一直减少时放弃性能:
Map
必须应用哈希表或其余机制来实现,均匀来说,这些机制提供的拜访工夫是汇合中元素数量的亚线性。
“ 亚线性 ” 只是意味着性能不会以与 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);
上面是测试后果:
100 行 | 10000 行 | 100000 行 | |
---|---|---|---|
Object | 0.023ms | 3.45ms | 89.9ms |
Map | 0.019ms | 2.1ms | 48.7ms |
17% | 39% | 46% |
请记住,这些后果在稍有不同的状况下可能会有相当大的差别,但总的来说,它们总体上合乎我的冀望。当解决绝对较少的我的项目时,Map
和对象之间的性能是相当的。但随着我的项目数量的减少,Map
开始拉开距离。这种性能上的亚线性变动开始显现出来。
WeakMaps 更无效地治理内存
有一个非凡版本的 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
中删除,开释了一点内存。这是一个我很观赏的性能,有助于放弃环境的内存更加整洁。
太长不看版
我喜爱为 DOM 节点应用Map
,因为:
- 节点自身能够作为键。我不须要先在每个节点上设置或读取独特的属性。
- 和具备大量成员的对象相比,
Map
(被设计成)更具备性能。 - 应用以节点为键的
WeakMap
意味着如果一个节点从 DOM 中被移除,条目将被主动垃圾回收。
本文译自:https://www.macarthur.me/posts/maps-for-dom-nodes
以上就是本文的全部内容,如果对你有所帮忙,欢送点赞、珍藏、转发~