JavaScript 系列 - WeakRef 和 FinalizationRegistry

449 阅读2分钟

WeakRef

WeakRef 对象允许您保留对另一个对象的弱引用,而不会阻止被弱引用对象被 GC 回收

实例方法

WeakRef.prototype.deref()

返回当前实例的 WeakRef 对象所绑定的 target 对象,如果该 target 对象已被 GC 回收则返回undefined

正确使用 WeakRef 对象需要仔细的考虑,最好尽量避免使用。

  • 如果您的代码刚刚为目标对象创建了 WeakRef,或者从 WeakRefderef 方法获取了目标对象,在当前 JavaScript job (包括在脚本作业末尾运行的任何 promise reaction 作业) 结束之前,不会回收该目标对象
  • 如果多个 WeakRefs 有相同的目标,那么他们的 target 对象是一样的。
  • 如果一个对象是 WeakReftarget,又在 FinalizationRegistry,那么该 target 就会在调用与注册表关联的任何清理回调之前或者同时被清理。如果清理回调调用对象的 WeakRef 上的 deref,它将收到 undefined
  • 你不能更改 WeakReftarget,它将始终是第一次指定的 target 或者在回收该 target 时会定义
  • WeakRef 可能永远不会从 deref 返回 undefined,即使没有什么东西能很好地保存 target,因为 GC 可能永远不会决定回收对象。

用途

缓存数据

// 缓存
function makeWeakCached(f) {
  const cache = new Map();
  return key => {
    const ref = cache.get(key);
    if (ref) {
      const cached = ref.deref();
      if (cached !== undefined) return cached;
    }

    const fresh = f(key);
    cache.set(key, new WeakRef(fresh));
    return fresh;
  };
}

const getImageCached = makeWeakCached(getImage);

FinalizationRegistry

FinalizationRegistry (清理器注册表) 对象可以让你在对象被垃圾回收时请求一个回调。

清理回调不应被用于必要的程序逻辑

正确使用需要仔细考虑,如果可能的话,最好避免使用。

const registry = new FinalizationRegistry((heldValue) => {
  // ....
});

实例方法

  • FinalizationRegistry.prototype.register(target, heldValue, unregisterToken)

    • target

      要注册的目标值

    • heldValue

      传递给清理回调函数的参数。不可以是 target

    • unregisterToken

      unregister 方法使用以注销目标值。必须是对象或未注册的 Symbol。如果未提供,则无法注销目标。

  • FinalizationRegistry.prototype.unregister(unregisterToken)

    注销目标

    class Thingy {
      static #cleanup = (label) => {
        //               ^^^^^−−−−− held value
        console.error(
          `The "release" method was never called for the object with the label "${label}"`,
        );
      };
      #registry = new FinalizationRegistry(Thingy.#cleanup);
    
      /**
       * Constructs a `Thingy` instance.
       * Be sure to call `release` when you're done with it.
       *
       * @param label A label for the `Thingy`.
       */
      constructor(label) {
        //                            vvvvv−−−−− held value
        this.#registry.register(this, label, this);
        //          target −−−−−^^^^         ^^^^−−−−− unregister token
      }
    
      /**
       * Releases resources held by this `Thingy` instance.
       */
      release() {
        this.#registry.unregister(this);
        //                        ^^^^−−−−− unregister token
      }
    }
    class Thingy {
      static #cleanup = (file) => {
        //               ^^^^−−−−− held value
        console.error(
          `The "release" method was never called for the "Thingy" for the file "${file.name}"`,
        );
      };
      #registry = new FinalizationRegistry(Thingy.#cleanup);
      #file;
    
      /**
       * Constructs a `Thingy` instance for the given file.
       * Be sure to call `release` when you're done with it.
       *
       * @param filename The name of the file.
       */
      constructor(filename) {
        this.#file = File.open(filename);
        //                            vvvvv−−−−− held value
        this.#registry.register(this, label, this.#file);
        //          target −−−−−^^^^         ^^^^^^^^^^−−−−− unregister token
      }
    
      /**
       * Releases resources held by this `Thingy` instance.
       */
      release() {
        if (this.#file) {
          this.#registry.unregister(this.#file);
          //                        ^^^^^^^^^^−−−−− unregister token
          File.close(this.#file);
          this.#file = null;
        }
      }
    }
    
    class Thingy {
      static #cleanup = (file) => {
        //               ^^^^−−−−− held value
        console.error(
          `The "release" method was never called for the "Thingy" for the file "${file.name}"`,
        );
      };
      #registry = new FinalizationRegistry(Thingy.#cleanup);
      #file;
    
      /**
       * Constructs a `Thingy` instance for the given file.
       * Be sure to call `release` when you're done with it.
       *
       * @param filename The name of the file.
       */
      constructor(filename) {
        this.#file = File.open(filename);
        //                            vvvvv−−−−− held value
        this.#registry.register(this, label, this.#file);
        //          target −−−−−^^^^         ^^^^^^^^^^−−−−− unregister token
      }
    
      /**
       * Releases resources held by this `Thingy` instance.
       */
      release() {
        if (this.#file) {
          this.#registry.unregister(this.#file);
          //                        ^^^^^^^^^^−−−−− unregister token
          File.close(this.#file);
          this.#file = null;
        }
      }
    }
    

用途

缓存

function makeWeakCached(f) {
  const cache = new Map();
  const cleanup = new FinalizationRegistry(key => {
    const ref = cache.get(key);
    if (ref && !ref.deref()) cache.delete(key);
  });

  return key => {
    const ref = cache.get(key);
    if (ref) {
      const cached = ref.deref();
      if (cached !== undefined) return cached;
    }

    const fresh = f(key);
    cache.set(key, new WeakRef(fresh));
    cleanup.register(fresh, key);
    return fresh;
  };
}

const getImageCached = makeWeakCached(getImage);