文章目录

JavaScript WeakRef弱引用在缓存场景中的实际应用

发布于 2026-04-25 22:21:54 · 浏览 6 次 · 评论 0 条

JavaScript WeakRef弱引用在缓存场景中的实际应用

在开发高流量或数据密集型的 Web 应用时,缓存是提升性能的关键手段。然而,使用传统的 JavaScript Map 或普通对象构建缓存,往往面临一个棘手问题:内存泄漏。如果不手动清理,缓存的数据会一直占用内存,直到进程崩溃。

JavaScript 提供的 WeakRef(弱引用)和 FinalizationRegistry(终结注册表)正是为了解决这一问题。它们允许创建“不阻止垃圾回收”的引用,让浏览器在内存紧张时自动清理缓存。


第一阶段:理解强引用与弱引用的区别

在编写代码前,必须先理解为什么传统的“强引用”会导致内存问题,以及“弱引用”是如何工作的。

  1. 分析强引用的弊端
    普通变量或 Map 中的键值对都是“强引用”。只要一个对象被强引用持有,垃圾回收器(GC)就绝对不敢回收它,即使你在代码中不再需要这个对象。

  2. 认识弱引用的特性
    WeakRef 创建的是对目标对象的“弱引用”。这意味着,如果对象没有被其他强引用持有(例如,业务逻辑中的变量已经指向了别处),垃圾回收器就有权在任意时刻回收该对象。此时,通过 WeakRef 访问对象可能会返回 undefined

  3. 明确适用场景
    弱引用非常适合用于缓存。缓存的特点是“有了更好,没有也能重新计算或获取”。使用弱引用,你可以放心地把数据存起来,而不必担心因为忘记清理而导致内存溢出。


第二阶段:构建基础弱引用缓存

下面我们创建一个简单的缓存类,使用 WeakRef 来存储计算结果,防止内存无限增长。

  1. 定义缓存类结构
    创建一个名为 WeakCache 的类。它内部维护一个普通的 Map,但 Map 中存储的不是数据本身,而是指向数据的 WeakRef 对象。

  2. 实现 set 方法
    编写方法将数据存入缓存。创建 一个 WeakRef 实例包裹目标数据,然后将其存入 Map

    class WeakCache {
      constructor() {
        this.cache = new Map();
      }
    
      set(key, value) {
        // 使用 WeakRef 包裹 value,建立弱引用
        this.cache.set(key, new WeakRef(value));
      }
    
      get(key) {
        const ref = this.cache.get(key);
        if (ref) {
          // 尝试通过 deref 获取原始对象
          const value = ref.deref();
          if (value !== undefined) {
            return value;
          } else {
            // 如果对象已被回收,清理 Map 中的 Key
            this.cache.delete(key);
          }
        }
        return undefined;
      }
    }
  3. 实现 get 方法
    编写读取逻辑。调用 ref.deref() 方法解引用。检查 返回值:

    • 如果返回对象,说明缓存命中,直接返回。
    • 如果返回 undefined,说明原对象已被垃圾回收。执行 this.cache.delete(key) 清理无效的键值对,并返回 undefined

第三阶段:自动化清理缓存键(进阶优化)

上述代码虽然解除了对数据的强引用,但 Map 中的 key(通常是字符串或数字)和 WeakRef 包装对象本身依然占据内存。当数据被回收后,Map 里会留下大量空的“壳”。我们需要引入 FinalizationRegistry 来自动清理这些“壳”。

  1. 理解 FinalizationRegistry 的作用
    FinalizationRegistry 允许你注册一个对象,并指定一个“回调函数”。当该对象被垃圾回收时,系统会异步调用这个回调函数。我们可以利用这个回调来清理 Map 中的 Key。

  2. 在类中集成注册表
    修改 WeakCache 类的构造函数,初始化 一个 FinalizationRegistry 实例。

    class AutoCleanWeakCache {
      constructor() {
        this.cache = new Map();
        // 初始化注册表,定义回调函数:被回收对象的 key 将被传入
        this.registry = new FinalizationRegistry((heldValue) => {
          // heldValue 是我们在注册时传入的 key
          this.cache.delete(heldValue);
          console.log(`Key [${heldValue}] 已自动清理`);
        });
      }
    
      set(key, value) {
        // 1. 将 WeakRef 存入 Map
        this.cache.set(key, new WeakRef(value));
        // 2. 将 value 对象注册到注册表
        // 参数:目标对象,注销时要保留的数据,注销时需要关联的 Token
        this.registry.register(value, key, key);
      }
    
      get(key) {
        const ref = this.cache.get(key);
        if (!ref) return undefined;
    
        const value = ref.deref();
        if (value === undefined) {
          this.cache.delete(key);
          return undefined;
        }
        return value;
      }
    }
  3. 注册对象与回调逻辑
    set 方法中,调用 this.registry.register(value, key, key)

    • 第一个参数是我们要监视的对象(即缓存的数据)。
    • 第二个参数是“保留值”,即 key。当对象被回收时,回调函数会收到这个 key,从而知道该删除 Map 中的哪一项。
    • 第三个参数是“注销令牌”,这里直接复用 key,以便后续如果需要手动注销时使用。

第四阶段:验证与测试

通过模拟内存压力来观察缓存是否自动失效。注意:垃圾回收的时机由浏览器引擎决定,通常无法强制立即触发,但我们可以通过模拟引用断开来观察逻辑。

  1. 实例化缓存对象
    创建 AutoCleanWeakCache 的实例 myCache

  2. 存入大数据
    定义一个大型对象或数组,调用 myCache.set('bigData', bigObject) 将其存入缓存。

    const myCache = new AutoCleanWeakCache();
    
    let bigData = { id: 1, data: new Array(1000000).fill('memory_heavy_data') };
    
    myCache.set('user_1', bigData);
    console.log('First get:', myCache.get('user_1')); // 应该能获取到数据
  3. 断开外部强引用
    将变量 bigData 设置为 null,切断外界对该对象的强引用。此时,该对象仅被 WeakRef 持有。

    bigData = null;
  4. 观察回收效果
    由于 JavaScript 的垃圾回收是惰性的,对象可能不会立即消失。执行 myCache.get('user_1')

    • 如果 GC 还没发生,你依然能取到值。
    • 当内存紧张或 GC 运行后,deref() 返回 undefined,且控制台会输出注册表中定义的“Key 已自动清理”日志,Map 也会随之变空。

第五阶段:使用弱引用的注意事项

虽然 WeakRef 很强大,但使用不当会导致难以排查的 Bug。请严格遵守以下原则。

  1. 不要将关键数据存放在 WeakRef 中
    由于对象随时可能被回收,禁止将“必须存在”的业务数据(如用户登录状态、关键配置项)仅用 WeakRef 保存。它只能作为“锦上添花”的缓存层。

  2. 避免依赖 deref 的返回值做逻辑判断
    不要写出 if (ref.deref() === undefined) { // 意味着被回收了 } 这样的代码。因为 undefined 也可能是你存入的合法值。正确的做法是仅将其作为缓存读取。

  3. 不要预判垃圾回收时机
    禁止尝试手动触发垃圾回收(虽然在 Node.js 某些模式下可行,但在浏览器中是不可能的)。代码逻辑必须假设对象可能随时存在,也可能随时消失。

  4. 清理注册表
    如果你的缓存键是动态生成的且数量巨大,记得在手动删除缓存项时,也要调用 registry.unregister(key),防止注册表中堆积过多的监听器。

    delete(key) {
      this.cache.delete(key);
      // 手动注销监听,防止内存泄漏
      this.registry.unregister(key);
    }

通过以上步骤,你已经构建了一个具备内存自动管理能力的缓存系统,既利用了缓存提升性能,又利用弱引用机制保证了系统的长期稳定性。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文