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

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

传统对象存在的劣势

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

javascriptCopy code





const person = {
  firstName: 'Alex',
  lastName: 'MacArthur',
  isACommunist: false
};

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

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

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后,如下所示:

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个元素的表格:

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

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,该对象会在你观察的引用被垃圾回收时触发回调。我们从几个列表项开始:

htmlCopy code
<ul>
  <li id="item1">first</li>
  <li id="item2">second</li>
  <li id="item3">third</li>
</ul>

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

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中移除第二个项并进行垃圾回收后,它的状态变为:

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节点,我强烈建议你在适当的场景下试一试。希望本文对你有所帮助,

谢谢阅读!

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY4rD5fg' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片