浅谈 JavaScript 中的弱引用(上)

1,238 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

一般来说,对象的引用在 JavaScript 中是强引用的,也就是只要存在一个只要对象的引用,这个对象就不会被垃圾收集掉。

const ref = { x: 12, y: 15 };

谈到 JavaScript 中弱引用,我们都会想起 WeakMaps 和 WeakSets 他们也是我们在 JavaScript 使用弱引用唯一途径,将一个对象作为键添加到 WeakMap 或 WeakSet 中并不能防止这些对象被垃圾收集。

const wm = new WeakMap();
{
  const ref = {};
  const metaData = 'foo';
  wm.set(ref, metaData);
  wm.get(ref);
  // → metaData
}

值得注意:可以把 WeakMap.prototype.set(ref, metaData) 看作是给对象ref添加属性值为metaData值的属性,所以也这是为什么要求 WeakMap 的键为对象,所以只要你对该对象的引用,就可以得到 metaData 。不过一旦不再有对该对象的引用,即使这个对象作为 WeakMap 的键引用,这个对象也将被垃圾收。对应于 WeakSet,可以把 WeakSet 看作是 WeakMap 的一个特例,其中所有的值都是布尔值。

JavaScript WeakMap 并不是真的弱,只要键对象还存在,实际上是对其内容的强引用。一旦键被垃圾收集,WeakMap 才会弱指其内容。这种关系的一个更准确的名字是ephemeron。

WeakRef是一个更高级的API,它提供了实际的弱引用,使人们能够看到一个对象的生命周期。让我们一起走过一个例子。

对于这个例子,假设我们正在开发一个聊天网络应用,使用 socket (网络套接字)与服务器进行通信。想象一个MovingAvg类,出于性能诊断的目的,保留了一组来自网络 socket 套接字的事件,以便计算出延迟的MovingAvg(简单移动平均值)。

class MovingAvg {
  constructor(socket) {
    this.events = [];
    this.socket = socket;
    this.listener = (ev) => { this.events.push(ev); };
    socket.addEventListener('message', this.listener);
  }

  compute(n) {
    // Compute the simple moving average for the last n events.
    // …
  }
}

通过一个 MovingAvgComponent 类来控制何时开始和停止监测延迟的 MovingAvgComponent 。

class MovingAvgComponent {
  constructor(socket) {
    this.socket = socket;
  }

  start() {
    this.movingAvg = new MovingAvg(this.socket);
  }

  stop() {
    // Allow the garbage collector to reclaim memory.
    this.movingAvg = null;
  }

  render() {
    // Do rendering.
    // …
  }
}

我们知道将所有的服务器信息保都存在一个 MovingAvg 实例中会消耗大量的内存,所以在停止监控时,需要将 this.movingAvg 清空,以便让垃圾回收将回收这些用于存储服务器信息的内存。

在 DevTools 的内存面板中检查后,发现内存根本就没有被回收!这是为什么?经验丰富的 Web 开发者可能已经发现了这个错误,事件监听器是强引用,必须显式将其删除。

让我们用对象之间调用关系图图。在调用 start()之后,在对象之间的关系图如下面,其中一个实心的箭头意味着强引用。从 MovingAvgComponent 实例通过实心箭头可以到达的所有对象都是不能被垃圾收的。

before_start.png

在调用stop()之后,虽然已经从 MovingAvgComponent 实例中删除了对 MovingAvg实例的强引用,但没有通过 socket 的清空监听器,所以我们下图这些类相互引用,MovingAvg 并不会在垃圾回收被回收。

after_start.png

因此,在 MovingAvg 实例中的监听器,通过引用这个,只要事件监听器没有被删除,就能保持整个实例的不会参与到垃圾回收。到目前为止,解决方案是通过一个处置方法手动来取消事件监听器的注册。

class MovingAvg {
  constructor(socket) {
    this.events = [];
    this.socket = socket;
    this.listener = (ev) => { this.events.push(ev); };
    socket.addEventListener('message', this.listener);
  }

  dispose() {
    this.socket.removeEventListener('message', this.listener);
  }

  // …
}

参考原文 Weak references and finalizers 文章