深入WeakMap和WeakSet:管理数据和防止内存泄漏

咱们做前端的,天天都在跟ObjectArray打交道。但ES6其实还给我们提供了另外两个非常有意思的数据结构:WeakMapWeakSet

说实话,我刚开始学这两个东西的时候,也觉得有点鸡肋。MapSet用得好好的,为啥非要搞个"Weak(弱鸡)"版本?而且它们还不能遍历,功能上好像还是阉割版。

直到有一次,我排查一个线上页面卡顿的问题,用Chrome的内存快照(Heap Snapshot)工具,发现了一些本该被销毁的DOM节点,却因为被我写的一个全局Map引用着,导致一直无法被垃圾回收(GC),造成了内存泄漏。

从那次之后,我才真正理解了WeakMapWeakSet存在的意义。它们不是MapSet的替代品,而是为了解决一类非常特殊的、关于内存管理问题而生的。


JavaScript的垃圾回收

在聊WeakMap之前,我们必须花一分钟,快速回顾一下JS的垃圾回收机制。

现代JS引擎(比如V8)的GC,主要依赖一个叫做 可达性(Reachability) 的概念。简单说就是:

  • 有一个"根"对象(比如全局的window对象,或者函数内部的局部变量)。
  • 从这个"根"出发,能通过引用链找到的对象,就是可达的,意味着它们是"活的",不能被回收。
  • 如果一个对象,从任何一个"根"出发,都无法找到它了,那它就是不可达的,GC就会在适当的时候把它清理掉,释放内存。

现在,问题来了。如果我们用一个普通的Map来存储一些数据,会发生什么?

JavaScript 复制代码
// 假设这是一个全局的Map,用来缓存一些DOM节点的信息
const domCache = new Map();

function cacheNodeInfo() {
  const element = document.getElementById('my-element');
  
  if (element) {
    // 我们把DOM节点作为key,存储了一些信息
    domCache.set(element, { someInfo: '...' });
  }
}

cacheNodeInfo();

// 后来,我们在某个操作中,把这个DOM节点从页面上移除了
document.getElementById('my-element').remove();

内存泄漏就发生在这里!

虽然你已经把#my-element这个DOM节点从页面上移除了,但因为全局的domCache这个Map,还通过key的方式,强引用着这个DOM节点对象,所以从GC的角度看,这个节点依然是可达的。

只要domCache不被销毁,这个DOM节点对象和它关联的数据,就会永远待在内存里,无法被回收。


WeakMap如何解决这个问题

WeakMap就是为了解决上面这个问题而生的。

它的核心特性就一个:它的key必须是对象,并且这个key是对对象的弱引用。

"弱引用"是什么意思?

它是一种不会被GC计入可达性判断的引用。GC看到一个对象只被弱引用指着,就会认为:"哦,没人需要你了",然后就会把它回收掉。

我们把上面的例子改成WeakMap

JavaScript 复制代码
const domCache = new WeakMap(); // 改成WeakMap

function cacheNodeInfo() {
  const element = document.getElementById('my-element');
  
  if (element) {
    domCache.set(element, { someInfo: '...' });
  }
}

cacheNodeInfo();

document.getElementById('my-element').remove();

// 在下一次GC发生时,由于#my-element这个DOM节点对象不再被任何强引用指向
// (页面上没了,domCache的引用又是弱的),
// GC就会把它连同domCache里与之关联的{ someInfo: '...' }一起回收掉。
// 内存泄漏的问题,就这么自动解决了。

WeakMap的这个特性,决定了它最典型的应用场景:

将一些元数据(metadata),附加到一个宿主对象上,并且不影响这个宿主对象的生命周期。

就像上面的例子,DOM节点是宿主,我们想给它附加一些额外信息,但我们不希望因为我们的附加行为,导致这个DOM节点无法被正常销毁。


我在哪里用到了WeakMap

1. 缓存DOM节点相关的数据

这是最经典的用法,就像上面的例子一样。比如,你需要给某个DOM节点关联一个复杂的配置对象、一个事件监听器集合、或者一个类的实例。

2. Vue 3响应式系统的核心

我后来去看Vue 3的源码,发现WeakMap在它的响应式系统里扮演了至关重要的角色。

Vue 3用Proxy来实现响应式。它需要一个地方,来存储"原始对象"和它对应的"响应式代理对象"之间的映射关系,避免重复代理。

JavaScript 复制代码
// 极简化的伪代码
const reactiveMap = new WeakMap();

function reactive(target) {
  // 如果已经代理过,直接返回缓存的代理
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target);
  }

  const proxy = new Proxy(target, ...);
  // 缓存映射关系
  reactiveMap.set(target, proxy);
  return proxy;
}

这里为什么必须用WeakMap?因为如果用普通Map,只要reactiveMap存在,所有被代理过的"原始对象"就永远不会被销毁,即使你的组件早就卸载了,页面上已经没人用它了。这会造成巨大的内存泄漏。


WeakSet呢?它有什么用?

理解了WeakMapWeakSet就很好懂了。

  • Set存储的是值的强引用集合。
  • WeakSet存储的是对象弱引用集合。

WeakSet的用处相对少一些,它主要用来标记一个对象,判断一个对象是否"在"或者"不在"某个集合里,同样不影响这个对象的垃圾回收。

一个常见的场景:防止重复处理

假设你有一个函数,需要处理一些DOM节点,但你想确保同一个节点在一次操作中只被处理一次。

JavaScript 复制代码
const processedNodes = new WeakSet();

function processNode(node) {
  if (processedNodes.has(node)) {
    // 已经处理过了,直接跳过
    return;
  }
  
  // ...这里是处理节点的逻辑...
  console.log('Processing node:', node);
  
  // 处理完,把它加到集合里,做个标记
  processedNodes.add(node);
}

const node1 = document.getElementById('node-1');
const node2 = document.getElementById('node-2');

processNode(node1); // 会执行
processNode(node2); // 会执行
processNode(node1); // 会跳过

这里用WeakSet的好处是,当node1从页面上被移除后,processedNodes里对它的弱引用会自动消失,不会造成内存泄漏。


什么时候该用它们?

你应该优先考虑使用WeakMapWeakSet的场景,都符合一个核心特征:

你想把一个"对象A"和一些"信息B"关联起来,但是"信息B"的生命周期,应该完全依赖于"对象A"的生命周期。当"对象A"被销- 时,"信息B"也应该被自动清理掉。

  • WeakMap:用在需要{ 对象A: 信息B }这种键值对关联的场景。
  • WeakSet:用在只需要标记[ 对象A ]是否存在的场景。

它们都不是万能的。因为它们不可遍历,功能也有限。但在合适的场景下,它们是解决内存泄漏问题。

希望这篇文章,能让你们对这两个低调的API,有更深的理解❀。

相关推荐
universe_016 分钟前
day25|学习前端js
前端·笔记
Zuckjet11 分钟前
V8 引擎的性能魔法:JSON 序列化的 2 倍速度提升之路
前端·chrome·v8
MrSkye12 分钟前
🔥React 新手必看!useRef 竟然不能触发 onChange?原来是这个原因!
前端·react.js·面试
wayman_he_何大民19 分钟前
初识机器学习算法 - AUM时间序列分析
前端·人工智能
juejin_cn20 分钟前
前端使用模糊搜索fuse.js和拼音搜索pinyin-match提升搜索体验
前端
....49244 分钟前
Vue3 + Element Plus 实现可搜索、可折叠、可拖拽的部门树组件
前端·javascript·vue.js
John_ToDebug1 小时前
Chromium base 库中的 Observer 模式实现:ObserverList 与 ObserverListThreadSafe 深度解析
c++·chrome·性能优化
teeeeeeemo1 小时前
如何做HTTP优化
前端·网络·笔记·网络协议·http
范范之交1 小时前
JavaScript基础语法two
开发语言·前端·javascript
界面开发小八哥2 小时前
DevExtreme Angular UI控件更新:引入全新严格类型配置组件
前端·ui·界面控件·angular.js·devexpress