了解 WeakSet 和 WeakMap:从内存管理到实战应用

21 阅读3分钟

一、 从 Set/Map 到 WeakSet/WeakMap

在 JavaScript 中,SetMap 是常用的集合类型:

  • Set:类似于数组,但成员唯一。
  • Map:键值对集合,键可以是任何类型。

它们都是强引用。只要集合实例存在,内部的对象就不会被垃圾回收(GC)。而 WeakSet/WeakMap 存储的是弱引用,专门用于解决内存泄漏问题。


二、 强引用与弱引用的博弈

  • 强引用:像铁链。只要拴着,对象就无法被回收。
  • 弱引用:像蛛丝。记录了对象位置,但 GC 清理内存时会直接忽略它。如果对象只剩下弱引用,就会被清理。

三、 内存消亡实验:揭开 GC 的面纱

实验准备

JavaScript

let tempObj = { name: 'Im still here' };
const ws = new WeakSet();
ws.add(tempObj);
tempObj = null; // 切断强引用

1. 实验:直接打印与手动回收

  • 操作:点击 Chrome Performance 面板的“垃圾桶”图标。
  • 现象:回收后打印,ws 变为空。

2. 实验:断点下的“顽固”对象

  • 现象:断点时点一次回收,对象可能还在;连续点多次,对象消失。

  • 原因

    • 分代回收:引擎可能只做了局部清理(Scavenge)。
    • 非确定性:GC 取决于内存压力和引擎算法。
    • 调试器干扰:断点状态可能产生隐藏强引用,需多次 Full GC 才能物理抹除。

四、 实际工程应用场景

1. 进度条(Loading)与按钮绑定

当业务要求点击按钮出现进度条,且进度条消失后其关联配置也应销毁时:

JavaScript

// 存储进度条的元数据(如开始时间、自定义配置)
const barConfigs = new WeakMap();

function createProgress(btn) {
    const progressBar = document.createElement('div');
    progressBar.className = 'progress-bar';
    document.body.appendChild(progressBar);

    // 绑定配置:Key 是 DOM 节点,Value 是配置对象
    barConfigs.set(progressBar, { 
        startTime: Date.now(), 
        theme: 'dark' 
    });

    // 模拟异步任务完成
    setTimeout(() => {
        const config = barConfigs.get(progressBar);
        console.log(`耗时: ${Date.now() - config.startTime}ms`);

        // 核心:移除 DOM 节点
        progressBar.remove();
        
        // 此时,progressBar 节点被销毁,barConfigs 里的记录也会被 GC 自动清理
    }, 2000);
}

2. Dialog 对话框及其内容的自动化销毁

在 Dialog 中,我们经常挂载一些高内存对象(如 Echarts 实例或大型数据缓存)。

JavaScript

// 存储 Dialog 关联的复杂实例
const dialogRegistry = new WeakMap();

function openDialog() {
    const dialog = document.createElement('div');
    dialog.className = 'custom-dialog';
    document.body.appendChild(dialog);

    // 模拟一个占用内存的大型对象(如三方库实例)
    const complexChart = {
        instance: "EchartsInstance",
        rawData: new Array(100000).fill('Heavy Data')
    };

    // 建立弱引用关联
    dialogRegistry.set(dialog, { chart: complexChart });

    // 点击遮罩层关闭
    dialog.onclick = () => {
        // 1. 获取关联数据进行清理动作(如注销监听)
        const data = dialogRegistry.get(dialog);
        console.log("清理实例:", data.chart.instance);

        // 2. 物理删除 DOM
        dialog.remove();
        
        // 3. 重点:由于是 WeakMap,只要 dialog 节点消失,
        //    它携带的 complexChart 大对象会自动进入回收名单
    };
}

五、 总结

  • Map/Set:用于持久、可遍历的业务数据存储。
  • WeakMap/WeakSet:用于临时、随动、私有的辅助数据关联。

通过这种“生命周期同步”的写法,你不再需要手动编写大量的 obj = null 或是 map.delete(key),代码的健壮性和可维护性会显著提升。