本文浅析一下为什么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(或其他字符串值)。这带来了一些额外的程序性开销,以便在需要时生成和读取这些值。
与之对应的是,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开始拉开距离。这种性能上的亚线性变化开始显现出来。
有一个特殊版本的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,因为:
本文译自:https://www.macarthur.me/posts/maps-for-dom-nodes
DOM是很慢的,其元素非常庞大,页面的性能问题鲜有由JS引起的,大部分都是由DOM操作引起的。虚拟的DOM的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地DOM操作。
浏览器解析HTML文档生成DOM树的过程,以下是一段HTML代码,以此为例来分析解析HTML文档的原理.解析HTML文档构建DOM树的理解过程可分为两个主要模块构成,即标签解析、DOM树构建
javascript获取DOM对象的多种方法:通过id获取 、通过class获取、通过标签名获取、通过name属性获取、通过querySelector获取、通过querySelectorAll获取等
遍历DOM节点常用一般用节点的 childNodes, firstChild, lastChild, nodeType, nodeName, nodeValue属性。在获取节点nodeValue时要注意,元素节点的子文本节点的nodeValue才是元素节点中文本的内容。
要构建自己的虚拟DOM,需要知道两件事。你甚至不需要深入 React 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟DOM的主要部分只需不到50行代码。
事件冒泡: 即事件开始时由最具体的元素(文档中嵌套层数最深的那个点)接收,事件捕获:不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件.与此同时,我们还需要了解dom事件绑定处理的几种方式:
先列出我的理解,然后再从具体的例子中说明:DOM操作本身应该是同步的(当然,我说的是单纯的DOM操作,不考虑ajax请求后渲染等);DOM操作之后导致的渲染等是异步的(在DOM操作简单的情况下,是难以察觉的)
早期由于浏览器厂商对于浏览器市场的争夺,各家浏览器厂商对同一功能的JavaScript的实现都不进相同,本节内容介绍JavaScript的DOM事件模型及事件处理程序的分类。
设置定义属性值 :data-value=.., 2.直接获取 3.通过this.$refs.***获取 1.目标DOM定义ref值: 2.通过 【this.$refs.***.属性名】 获取相关属性的值: this.$refs.*** 获取到对应的元素 ...
框架用多了,你还记得那些操作 DOM 的纯 JS 语法吗?看看这篇文章,来回顾一下~ 操作 className,addClass给元素增加 class,使用 classList 属性
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!