FinalizationRegistry

**FinalizationRegistry** 对象允许您在值被垃圾回收时请求回调。

描述

FinalizationRegistry 提供了一种方法,可以请求在某个时间点调用清理回调,此时已注册到注册表的某个值已被回收(垃圾回收)。(清理回调有时被称为终结器。)

注意:清理回调不应用于基本程序逻辑。有关详细信息,请参阅关于清理回调的说明

您通过传入回调来创建注册表

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

然后,您可以通过调用register 方法来注册任何您希望为其提供清理回调的值,并为其传递一个持有值

js
registry.register(target, "some value");

注册表不会对值保持强引用,因为这会违背其目的(如果注册表对其保持强引用,则该值永远不会被回收)。在 JavaScript 中,对象和未注册的符号是可以被垃圾回收的,因此它们可以作为目标或令牌注册到FinalizationRegistry 对象中。

如果target 被回收,则您的清理回调可能会在某个时间点使用您为其提供的持有值(上述示例中的"some value")被调用。持有值可以是您喜欢的任何值:基本类型或对象,甚至是undefined。如果持有值是一个对象,则注册表会对其保持引用(以便稍后将其传递给您的清理回调)。

如果您可能希望稍后取消注册已注册的目标值,则可以传递第三个值,即稍后在调用注册表的unregister 函数以取消注册该值时将使用的取消注册令牌。注册表仅对取消注册令牌保持弱引用。

通常使用目标值本身作为取消注册令牌,这完全没问题

js
registry.register(target, "some value", target);
// …

// some time later, if you don't care about `target` anymore, unregister it
registry.unregister(target);

不过,它不必是相同的值;它可以是不同的值

js
registry.register(target, "some value", token);
// …

// some time later
registry.unregister(token);

尽可能避免

正确使用FinalizationRegistry 需要仔细考虑,并且如果可能,最好避免使用它。同样重要的是,要避免依赖规范未保证的任何特定行为。垃圾回收何时、如何以及是否发生取决于任何给定 JavaScript 引擎的实现。您在一个引擎中观察到的任何行为在另一个引擎中、同一引擎的另一个版本中,甚至在同一引擎的相同版本中略有不同的情况下都可能不同。垃圾回收是一个难题,JavaScript 引擎实现者不断改进和完善其解决方案。

以下是介绍FinalizationRegistry提案中作者包含的一些具体要点

垃圾收集器 非常复杂。如果应用程序或库依赖于 GC 清理 WeakRef 或及时、可预测地调用终结器[清理回调],则可能会感到失望:清理可能比预期晚得多,或者根本不会发生。可变性的来源包括

  • 即使一个对象和另一个对象同时变得不可访问,一个对象也可能比另一个对象快得多地被垃圾回收,例如,由于分代收集。
  • 垃圾回收工作可以使用增量和并发技术分解成多个时间段。
  • 各种运行时启发式方法可用于平衡内存使用和响应能力。
  • JavaScript 引擎可能会持有对看起来不可访问的事物的引用(例如,在闭包或内联缓存中)。
  • 不同的 JavaScript 引擎可能会以不同的方式执行这些操作,或者同一引擎可能会在其不同版本中更改其算法。
  • 复杂因素可能会导致对象保持活动状态超出预期的时间,例如与某些 API 一起使用。

关于清理回调的说明

  • 开发人员不应依赖清理回调来实现基本程序逻辑。清理回调可能有助于在程序执行过程中减少内存使用,但在其他情况下不太可能有用。
  • 如果您的代码刚刚将某个值注册到注册表,则该目标在当前 JavaScript 作业结束之前不会被回收。有关详细信息,请参阅关于 WeakRef 的说明
  • 即使是执行垃圾回收的符合标准的 JavaScript 实现也不需要调用清理回调。它何时以及是否这样做完全取决于 JavaScript 引擎的实现。当已注册的对象被回收时,任何与之相关的清理回调都可能在当时被调用,或者稍后被调用,或者根本不会被调用。
  • 主要实现可能会在执行期间的某个时间点调用清理回调,但这可能是相关对象被回收后很久才调用。此外,如果某个对象注册在两个注册表中,则不能保证这两个回调会彼此相邻地调用——一个可能会被调用而另一个永远不会被调用,或者另一个可能会在很久以后被调用。
  • 还有一些情况,即使是通常会调用清理回调的实现也不太可能调用它们
    • 当 JavaScript 程序完全关闭时(例如,在浏览器中关闭选项卡)。
    • FinalizationRegistry 实例本身不再可由 JavaScript 代码访问时。
  • 如果WeakRef 的目标也位于FinalizationRegistry 中,则WeakRef 的目标会在与注册表关联的任何清理回调被调用之前或同时被清除;如果您的清理回调对该对象的WeakRef 调用deref,它将收到undefined

构造函数

FinalizationRegistry()

创建一个新的FinalizationRegistry 对象。

实例属性

这些属性定义在FinalizationRegistry.prototype 上,并由所有FinalizationRegistry 实例共享。

FinalizationRegistry.prototype.constructor

创建实例对象的构造函数。对于FinalizationRegistry 实例,初始值为FinalizationRegistry 构造函数。

FinalizationRegistry.prototype[Symbol.toStringTag]

[Symbol.toStringTag] 属性的初始值为字符串"FinalizationRegistry"。此属性用于Object.prototype.toString()

实例方法

FinalizationRegistry.prototype.register()

将对象注册到注册表中,以便在对象被垃圾回收时/如果对象被垃圾回收时获得清理回调。

FinalizationRegistry.prototype.unregister()

从注册表中取消注册对象。

示例

创建新的注册表

您通过传入回调来创建注册表

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

注册要清理的对象

然后,您可以通过调用 `register` 方法并传入对象及其对应的持有值来注册任何需要清理回调的对象。

js
registry.register(theObject, "some value");

回调永远不会同步调用

无论您对垃圾回收器施加多少压力,清理回调都不会同步调用。对象可能会同步回收,但回调始终会在当前作业完成后的某个时间调用。

js
let counter = 0;
const registry = new FinalizationRegistry(() => {
  console.log(`Array gets garbage collected at ${counter}`);
});

registry.register(["foo"]);

(function allocateMemory() {
  // Allocate 50000 functions — a lot of memory!
  Array.from({ length: 50000 }, () => () => {});
  if (counter > 5000) return;
  counter++;
  allocateMemory();
})();

console.log("Main job ends");
// Logs:
// Main job ends
// Array gets garbage collected at 5001

但是,如果您在每次分配之间稍作停顿,则回调可能会更快地被调用。

js
let arrayCollected = false;
let counter = 0;
const registry = new FinalizationRegistry(() => {
  console.log(`Array gets garbage collected at ${counter}`);
  arrayCollected = true;
});

registry.register(["foo"]);

(function allocateMemory() {
  // Allocate 50000 functions — a lot of memory!
  Array.from({ length: 50000 }, () => () => {});
  if (counter > 5000 || arrayCollected) return;
  counter++;
  // Use setTimeout to make each allocateMemory a different job
  setTimeout(allocateMemory);
})();

console.log("Main job ends");

无法保证回调会更快地被调用,或者是否会根本被调用,但有可能记录的消息的计数器值小于 5000。

规范

规范
ECMAScript 语言规范
# sec-finalization-registry-objects

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅