【译】DOM节点管理进化:Map和WeakMap的巧妙应用

原文链接:www.macarthur.me/posts/maps-...

传统对象存在的劣势

在JavaScript中,我们经常使用普通的对象来存储键/值数据,主打一个------易读易懂:

arduino 复制代码
javascriptCopy code
const person = {
  firstName: 'Alex',
  lastName: 'MacArthur',
  isACommunist: false
};

然而,当处理大型实体,其属性频繁被读取、更改和添加时,人们越来越倾向于使用Map。有很多好处,特别是在性能敏感的情况下,或者在插入顺序非常重要的情况下。

最近,我意识到我特别喜欢在处理大量DOM节点时使用Map(以及WeakMap)。这个想法是在阅读Caleb Porzio的一篇博客文章时产生的。在这篇文章中,他介绍了一个由1万个表格行组成的虚构表格,其中一个行可以是"active(激活)"的状态。为了管理选中的不同行的状态,他使用了一个对象作为键/值存储。

ini 复制代码
javascriptCopy code
import { ref, watchEffect } from 'vue';

let rowStates = {};
let activeRow;

document.querySelectorAll('tr').forEach((row) => {
    // 设置行状态。
    rowStates[row.id] = ref(false);

    row.addEventListener('click', () => {
        // 更新行状态。
        if (activeRow) rowStates[activeRow].value = false;

        activeRow = row.id;

        rowStates[row.id].value = true;
    });

    watchEffect(() => {
        // 读取行状态。
        if (rowStates[row.id].value) {
            row.classList.add('active');
        } else {
            row.classList.remove('active');
        }
    });
});

这段代码完美地完成了任务。但是,它使用一个对象作为大型哈希表,所以关联值的键必须是一个字符串,这就需要在每个项目上存在一个唯一的ID(或其他字符串值),这增加了程序上的额外开销,无论是在生成还是读取这些值时都是如此。

相对于传统Object的优势

我们可以考虑下,其实任何对象都可以成为Map的键。因此,我们可以直接使用HTML节点本身作为键。所以,将代码改为使用Map后,如下所示:

ini 复制代码
javascriptCopy code
import { ref, watchEffect } from 'vue';

let rowStates = new Map();
let activeRow;

document.querySelectorAll('tr').forEach((row) => {
    rowStates.set(row, ref(false));

    row.addEventListener('click', () => {
        if (activeRow) rowStates.get(activeRow).value = false;

        activeRow = row;

        rowStates.get(activeRow).value = true;
    });

    watchEffect(() => {
        if (rowStates.get(row).value) {
            row.classList.add('active');
        } else {
            row.classList.remove('active');
        }
    });
});

最明显的好处是我无需担心每行上是否存在唯一的ID。节点引用本身就是唯一的,所以它们可以直接作为键。因为这样,不需要设置或读取任何属性。代码更加简单和健壮。

对于不同数量级map和Object的性能测试

另外,读写操作通常更高效。我在原文中进行了一些简单的性能测试。首先,我创建了一个包含10,000个元素的表格:

ini 复制代码
javascriptCopy code
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循环中多次运行该过程,然后计算写入和读取的平均时间。

ini 复制代码
javascriptCopy code
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 %更快
100 0.023ms 0.019ms 17%
10,000 3.45ms 2.1ms 39%
100,000 89.9ms 48.7ms 46%

请注意,这些结果可能会在稍微不同的环境下有所不同,但总体而言,通常情况下,Map相对于对象性能的提升是显著的。Map在大型数据集上的表现非常出色。

WeakMap的作用

此外,WeakMap在更有效地管理内存方面表现出色。WeakMap是Map接口的一个特殊版本,旨在更好地管理内存。它通过对键持有"弱引用",这意味着如果这些键不再在其他地方有引用,它们将被垃圾回收。对于DOM节点,这一点尤其有用。这样,当节点不再需要时,整个条目会自动从WeakMap中删除,释放更多内存。

为了演示这一点,我们使用FinalizationRegistry,该对象会在你观察的引用被垃圾回收时触发回调。我们从几个列表项开始:

xml 复制代码
htmlCopy code
<ul>
  <li id="item1">first</li>
  <li id="item2">second</li>
  <li id="item3">third</li>
</ul>

然后,我们将这些项放入WeakMap,并将item2注册到FinalizationRegistry以进行监视。接下来,我们移除它,当它被垃圾回收时,回调函数将被触发,我们可以看到WeakMap的变化。

javascript 复制代码
javascriptCopy code
(async () => {
  const listMap = new WeakMap();

  // 将每个项放入WeakMap。
  document.querySelectorAll('li').forEach((node) => {
    listMap.set(node, node.id);
  });

  const registry = new FinalizationRegistry((heldValue) => {
    // 垃圾回收发生了!
    console.log('After collection:', heldValue);
  });

  registry.register(document.getElementById('item2'), listMap);

  console.log('Before collection:', listMap);

  // 移除节点,释放引用!
  document.getElementById('item2').remove();

  // 周期性地创建大量对象以触发垃圾回收。
  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中移除第二个项并进行垃圾回收后,它的状态变为:

bash 复制代码
bashCopy code
Before collection: WeakMap {<li id="item1"> => "item1", <li id="item2"> => "item2", <li id="item3"> => "item3"}
After collection: WeakMap {<li id="item1"> => "item1", <li id="item3"> => "item3"}

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

综上所述

用Map处理DOM节点,原因有以下几点:

  1. 节点本身可以直接作为键,无需生成和读取唯一的属性。
  2. 在处理大量对象时,Map通常(设计为)更高效。
  3. 使用WeakMap与节点作为键,可以自动进行垃圾回收,释放不再需要的内存。

在处理大型DOM节点集合时,Map和WeakMap是非常有用的工具,它们提供了更简单、更高效、更灵活的方式来管理数据,并可以更好地管理内存,从而使得应用程序性能更出色。如果你还没有尝试过使用它们来处理DOM节点,我强烈建议你在适当的场景下试一试。希望本文对你有所帮助,

谢谢阅读!

相关推荐
穷人小水滴24 分钟前
使用 epub 在手机快乐阅读
javascript·deno·科幻
ganshenml42 分钟前
【Web】证书(SSL/TLS)与域名之间的关系:完整、通俗、可落地的讲解
前端·网络协议·ssl
这是个栗子1 小时前
npm报错 : 无法加载文件 npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
爱学习的程序媛2 小时前
《深入浅出Node.js》核心知识点梳理
javascript·node.js
HIT_Weston2 小时前
44、【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 分析(一)
前端·ubuntu·gitlab
华仔啊2 小时前
Vue3 如何实现图片懒加载?其实一个 Intersection Observer 就搞定了
前端·vue.js
JamesGosling6663 小时前
深入理解内容安全策略(CSP):原理、作用与实践指南
前端·浏览器
不要想太多3 小时前
前端进阶系列之《浏览器渲染原理》
前端
Robet3 小时前
TS和JS成员变量修饰符
javascript·typescript
g***96903 小时前
Node.js npm 安装过程中 EBUSY 错误的分析与解决方案
前端·npm·node.js