本文浅析一下为什么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行
Object0.023ms3.45ms89.9ms
Map0.019ms2.1ms48.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

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