如何使用Map处理Dom节点

如何,使用,map,处理,dom,节点 · 浏览次数 : 84

小编点评

**代码分析** **Map** * 使用**WeakMap****来管理**节点**的**键**。 * **节点**的**值**在**移除**后自动从**Map**中删除。 * 由于**节点**的**键**在**移除**后自动从**Map**中删除,**Map**中的**条目**自动从**Map**中删除。 * **Map**的性能更**优**于**对象**。 **WeakMap** * 使用****弱引用****来管理**节点**的**键**。 * **节点**的**值**在**移除**后自动从**WeakMap**中删除。 * 由于**节点**的**值**在**移除**后自动从**WeakMap**中删除,**WeakMap**中的**条目**自动从**WeakMap**中删除。 * **WeakMap**的性能更**优**于**对象**。 **最终** * 使用**FinalizationRegistry****来**监控****节点**的****变化**。 * **监控****节点的**变化**时,**FinalizationRegistry**会触发回调,**打印****** collection** 的**信息**。 * ****监控****节点的**变化**时,**FinalizationRegistry**会触发回调,**打印****** collection** 的**信息**。

正文

本文浅析一下为什么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中被移除并发生垃圾回收后,它看起来有点不同:

image.png

由于节点引用不再存在于DOM中,整个条目都被从WeakMap中删除,释放了一点内存。这是一个我很欣赏的功能,有助于保持环境的内存更加整洁。

太长不看版

我喜欢为DOM节点使用Map,因为:

  • 节点本身可以作为键。我不需要先在每个节点上设置或读取独特的属性。
  • 和具有大量成员的对象相比,Map(被设计成)更具有性能。
  • 使用以节点为键的WeakMap意味着如果一个节点从DOM中被移除,条目将被自动垃圾回收。

以上就是本文的全部内容,如果对你有所帮助,欢迎点赞、收藏、转发~

与如何使用Map处理Dom节点相似的内容:

如何使用Map处理Dom节点

本文浅析一下为什么`Map`(和WeakMap)在处理大量DOM节点时特别有用。 我们在JavaScript中使用了很多普通的、古老的对象来存储键/值数据,它们处理的非常出色: ```jsx const person = { firstName: 'Alex', lastName: 'MacArth

还在stream中使用peek?不要被这些陷阱绊住了

简介 自从JDK中引入了stream之后,仿佛一切都变得很简单,根据stream提供的各种方法,如map,peek,flatmap等等,让我们的编程变得更美好。 事实上,我也经常在项目中看到有些小伙伴会经常使用peek来进行一些业务逻辑处理。 那么既然JDK文档中说peek方法主要是在调试的情况下使

分享一个关于Avl树的迭代器算法

1 研究过程 前段时间在研究avl树的迭代实现,在节点不使用parent指针的情况下,如何使用堆栈来实现双向地迭代。我参考了网络上的大部分迭代器实现,要么是使用了parent指针(就像c++的map容器中的迭代算法),要么就是前中后序遍历,没找到一种真正意义上可以双向迭代的算法,于是乎在我的不屑努力

11.1 C++ STL 应用字典与列表

C++ STL 标准模板库提供了丰富的容器和算法,这些模板可以灵活组合使用,以满足不同场景下的需求。本章内容将对前面学习的知识进行总结,并重点讲解如何灵活使用STL中的vector和map容器,以及如何结合不同的算法进行组合。通过灵活组合使用这些容器和算法,能够满足不同场景下的需求,实现高效的数据处理和操作。STL的设计思想是将数据结构和算法进行分离,使得开发者能够更加专注于解决问题,提高了代码的

6.1 C++ STL 序列映射容器

Map/Multimap 映射容器属于关联容器,它的每个键对应着每个值,容器的数据结构同样采用红黑树进行管理,插入的键不允许重复,但值是可以重复的,如果使用`Multimap`声明映射容器,则同样可以插入相同的键值。Map中的所有元素都会根据元素的键值自动排序,所有的元素都是一个`Pair`同时拥有实值和键值,Pair的第一个元素被视为键值,第二个元素则被视为实值,Map 容器中不允许两个元素有相

[转帖]nginx中map使用方法

场景: 匹配请求 url 的参数,如果参数是 debug 则设置 $foo = 1 ,默认设置 $foo = 0 map $args $foo { default 0; debug 1;} $args 是nginx内置变量,就是获取的请求 url 的参数。 如果 $args 匹配到 debug 那么

golang sync.Map 与使用普通的 map 的区别

使用sync.Map与普通的Go map主要有以下几点区别: 1. 并发安全性 普通map: 在没有外部同步的情况下,不是并发安全的。在多goroutine访问时,如果没有适当的锁或其他同步机制保护,可能会导致数据竞争和未定义行为。 sync.Map: 是并发安全的。它内部实现了必要的同步机制,允许

[转帖]nginx的map指令

一 ngx_http_map_module模块 1) map 指令是由 'ngx_http_map_module 模块'提供的,默认情况下安装 nginx 都会'安装'该模块. 2) map 的主要作用是'创建自定义变量',通过使用 nginx 的'内置'变量,去'匹配'某些特定规则;如果匹配成功则

Java SpringBoot 加载 yml 配置文件中字典项

实际项目中,如果将该类信息放配置文件中的话,一般会结合Nocas一起使用 将字典数据,配置在 yml 文件中,通过加载yml将数据加载到 Map中 Spring Boot 中 yml 配置、引用其它 yml 中的配置。# 在配置文件目录(如:resources)下新建application-xxx

easyExcel多行表头设定不同样式和特定单元格设定样式的实现

前言 有个需求,需要设置Excel导出的样式,样式如下图所示,有三个表头行,第一个表头行需要加粗和灰色背景,另外两个表头行使用另外的样式,并且当测试结果单元格出现x或者未通过的时候,设置其为红色字体。 实现步骤 写入ExcelSheet的部分代码 for (Map.Entry