关于前端:前端开发必备Maps与WeakMaps在DOM节点管理中的妙用

39次阅读

共计 4727 个字符,预计需要花费 12 分钟才能阅读完成。

本文首发于微信公众号:大迁世界, 我的微信:qq449245884,我会第一工夫和你分享前端行业趋势,学习路径等等。
更多开源作品请看 GitHub https://github.com/qq449245884/xiaozhi,蕴含一线大厂面试残缺考点、材料以及我的系列文章。

快来收费体验 ChatGpt plus 版本的,咱们出的钱
体验地址:https://chat.waixingyun.cn
能够退出网站底部技术群,一起找 bug.

这篇文章探讨了应用 MapsWeakMaps 解决 DOM 节点的劣势。MapsWeakMaps 是十分实用的工具,尤其在解决大量 DOM 节点时,它们施展着重要作用。

文章作者认为,应用 MapsWeakMaps 解决 DOM 节点有以下几个长处。首先,它们能够不便地存储和检索数据。与其余数据结构相比,Maps 和 WeakMaps 能够更简洁地组织和查找相干数据。其次,它们能够帮忙开发者更好地治理内存。当不再须要某个 DOM 节点时,WeakMaps 能够主动开释与该节点相干的内存,从而进步程序的性能。最初,应用 Maps 和 WeakMaps 能够进步代码的可读性和可维护性。将 DOM 节点与相干数据关联起来,有助于使代码更清晰易懂。

上面是注释:

在 JavaScript 中,咱们常常应用一般的对象来存储键 / 值数据,它们十分善于这项工作 – 清晰易读:

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

然而,当咱们开始解决常常被读取、更改和增加属性的较大实体时,更偏向于应用 Maps。因为在某些状况下,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(或其余字符串值)。这带来了一些额定的编程开销,须要在须要时生成和读取这些值。

任何对象都能够作为键

相同,应用 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,以便随着我的项目数量的减少而增长:

Maps must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements in the collection.

“Sublinear”的意思是性能不会随着 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);

我用不同的行大小运行了这个测试:

请记住,即便是略微不同的状况,这些后果也可能会有很大的差别,但总体而言,它们通常合乎我的预期。在解决绝对较少的我的项目时,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 中删除,从而开释了更多的内存。这是一个很 nice 性能,有助于使环境的内存更加整洁。

代码部署后可能存在的 BUG 没法实时晓得,预先为了解决这些 BUG,花了大量的工夫进行 log 调试,这边顺便给大家举荐一个好用的 BUG 监控工具 Fundebug。

交换

有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

正文完
 0