乐趣区

关于javascript:如何使用Map处理Dom节点

本文浅析一下为什么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

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

退出移动版