环球热点评!如何使用Map处理Dom节点

来源:博客园   时间:2023-05-25 00:28:29

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

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,每当你所监听的引用被垃圾回收时,它就会触发一个回调(我从未想到会发现这样的好东西)。我们将从几个列表项开始:

  • first
  • second
  • third

接下来,我们将把这些项放在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中被移除,条目将被自动垃圾回收。

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

关键词:

文章推荐

  • 香港中文大学(深圳)校长徐扬生:用4年的时间等到花开

    当你们急于一件事情的结果,或者焦虑于为何落后于人的时候,不妨想一想校园里的蓝花楹,我们用了4年的时间等到了花开。只要你认准了路,慢

    深圳商报 2022-05-23
  • 哈尔滨铁路迎节后返程高峰 推出复工专列服务

    中新网哈尔滨2月6日电 (周晓舟 记者 史轶夫)中国铁路哈尔滨局有限公司6日发布消息,哈尔滨铁路迎来春节后返程客流高峰,6日至7日预

    中新网 2022-02-07
  • 冬奥动车组设5G超高清演播室 “瑞雪迎春”号智能化人性化结合

    中新网北京2月6日电 (记者 刘文曦)在时速350公里的高铁列车上首设5G超高清演播室,为北京冬奥会量身定制的新型奥运版智能复兴号动车组瑞

    中新网 2022-02-07
  • 中欧班列“签证官”:日行10公里 用锤子“听诊”

    (新春走基层)中欧班列“签证官”:日行10公里 用锤子“听诊”  中新网郑州2月6日电 题:中欧班列“签证官”:日行10公里,用锤子“

    中新网 2022-02-07
  • 西湖守兰人的春节美丽故事:花苞为伴 手留余香

    中新网杭州2月6日电 (记者 谢盼盼)守望花苞,这是西湖守兰人许晔的春节故事,春节正是兰花花苞开花的重要时期。  今年春节里,浙江

    中新网 2022-02-07
  • 广告

    X 关闭

    X 关闭

  • 众测
  • more+

    环球热点评!如何使用Map处理Dom节点

    本文浅析一下为什么`Map`(和WeakMap)在处理大量DOM节点时特别有用。我们在JavaScript中使用了很多普通的

    七百年钓鱼城:创5A和申遗 一个都不能少|今日快看

    中国山东网-感知山东5月24日讯(记者刘锬)登上钓鱼城,仿佛还能望见历史的烽烟穿越时空而来。700多年前,钓

    交通运输部在南沙群岛海域设置航标 全球最资讯

    交通运输部在南沙群岛海域设置航标:交通运输部官微5月24日消息,近日,为保障船舶航行及作业安全,交通运

    港股收评:指数低开低走,恒生科技指数跌近2%,新能源、黄金等板块逆势上涨

    南方财经5月24日电,5月24日下午,港股收盘,行情全天低开低走,恒生指数收跌1 62%,恒生科技指数收跌1 99%