JavaScript 中的 WeakRef 与 FinalizationRegistry
字数 1150 2025-12-09 22:58:39

JavaScript 中的 WeakRef 与 FinalizationRegistry

WeakRef 和 FinalizationRegistry 是 ECMAScript 2021(ES12)中引入的两个与内存管理相关的重要 API,它们允许开发者与 JavaScript 的垃圾回收机制进行更精细的交互,特别是在管理对象生命周期和清理资源时。

知识描述:

在传统的 JavaScript 中,一个对象只要被引用,垃圾回收器就不会回收它。WeakRef 允许你创建一个对对象的“弱引用”,这意味着它不会阻止该对象被垃圾回收。FinalizationRegistry 则允许你注册一个回调,当某个对象被垃圾回收时,这个回调会被执行,以便进行一些清理工作。

解题过程循序渐进讲解:

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

在 JavaScript 中,通常的变量引用是“强引用”。

let obj = { data: "important" }; // 这是一个强引用
let ref = obj; // 这是另一个强引用指向同一个对象

obj = null; // 移除了一个强引用
// 但对象仍然被 ref 强引用着,所以不会被垃圾回收

只要存在至少一个强引用,对象就不会被垃圾回收。而弱引用则不同:

  • 弱引用不会阻止垃圾回收
  • 如果对象只被弱引用持有,它会被垃圾回收
  • 当对象被回收后,弱引用会自动变为 undefined(或根据实现返回 undefined

第二步:使用 WeakRef 创建弱引用

// 创建一个普通对象
let targetObject = { 
  id: 1, 
  data: new Array(1000000).fill('x') // 大量内存占用
};

// 创建对 targetObject 的弱引用
let weakRef = new WeakRef(targetObject);

// 通过弱引用获取原对象
let derefObject = weakRef.deref();
console.log(derefObject); // 输出: {id: 1, data: [...]}

// 现在移除所有强引用
targetObject = null;
derefObject = null;

// 手动触发垃圾回收(在浏览器中通常不可用,这里仅为演示)
// 在实际中,垃圾回收会在某个时刻自动发生
// gc(); // 假设的强制垃圾回收

// 垃圾回收后,弱引用变为空
setTimeout(() => {
  console.log(weakRef.deref()); // 可能输出: undefined
}, 1000);

第三步:WeakRef 的使用场景

WeakRef 主要用于缓存和监控等场景,你希望持有对某个对象的引用,但又不希望阻止它被回收。

class ExpensiveObject {
  constructor(data) {
    this.data = data;
    this.result = this.expensiveCalculation();
  }
  
  expensiveCalculation() {
    // 模拟耗时计算
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += Math.sqrt(i);
    }
    return result;
  }
}

// 使用 WeakRef 的缓存
class CacheWithWeakRef {
  constructor() {
    this.cache = new Map(); // 存储 WeakRef
  }
  
  getOrCreate(key, createFn) {
    let cachedRef = this.cache.get(key);
    let cachedValue = cachedRef?.deref();
    
    if (cachedValue) {
      return cachedValue; // 缓存命中
    }
    
    // 创建新对象并缓存弱引用
    let newValue = createFn();
    this.cache.set(key, new WeakRef(newValue));
    return newValue;
  }
  
  // 清理已回收的缓存项
  cleanup() {
    for (let [key, ref] of this.cache.entries()) {
      if (!ref.deref()) {
        this.cache.delete(key);
      }
    }
  }
}

第四步:理解 FinalizationRegistry

FinalizationRegistry 允许你注册一个清理回调,当对象被垃圾回收时执行。

// 创建一个 FinalizationRegistry,指定清理回调
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`对象已被回收,清理值: ${heldValue}`);
  // 在这里可以执行一些清理操作,如关闭文件、释放外部资源等
});

// 创建一个对象
let resource = {
  id: 1,
  data: new Array(1000).fill('x')
};

// 注册到 FinalizationRegistry
// 参数1: 要监控的对象
// 参数2: 关联的值(held value),会在清理回调中传递
// 参数3: 可选的取消令牌(可选)
let token = {};
registry.register(resource, `Resource ${resource.id}`, token);

// 移除强引用
resource = null;

// 当垃圾回收发生时,会输出: "对象已被回收,清理值: Resource 1"

第五步:FinalizationRegistry 的完整使用

// 示例:管理文件句柄的清理
class FileHandlerManager {
  constructor() {
    // 创建 FinalizationRegistry 来清理文件句柄
    this.registry = new FinalizationRegistry((filePath) => {
      console.log(`清理未关闭的文件: ${filePath}`);
      // 在实际应用中,这里会调用系统API关闭文件
      this.cleanupFile(filePath);
    });
    
    this.tokens = new Map(); // 存储 token 用于取消注册
  }
  
  cleanupFile(filePath) {
    // 模拟关闭文件的清理操作
    console.log(`执行清理操作: 关闭 ${filePath}`);
  }
  
  openFile(filePath) {
    // 模拟打开文件
    const fileHandle = {
      path: filePath,
      data: `Content of ${filePath}`,
      close: function() {
        console.log(`手动关闭文件: ${filePath}`);
        // 关闭文件的操作
      }
    };
    
    // 创建取消令牌
    const token = { filePath };
    
    // 注册到 FinalizationRegistry
    this.registry.register(fileHandle, filePath, token);
    this.tokens.set(fileHandle, token);
    
    return fileHandle;
  }
  
  closeFile(fileHandle) {
    // 手动关闭文件
    fileHandle.close();
    
    // 取消 FinalizationRegistry 的注册
    const token = this.tokens.get(fileHandle);
    if (token) {
      this.registry.unregister(token);
      this.tokens.delete(fileHandle);
    }
  }
}

// 使用示例
const manager = new FileHandlerManager();
let file = manager.openFile('/path/to/file.txt');

// 如果忘记调用 closeFile,垃圾回收时会自动清理
// file = null; // 移除引用,垃圾回收时会触发清理回调

// 或者手动关闭
manager.closeFile(file);

第六步:重要注意事项和最佳实践

  1. 垃圾回收时机不确定:FinalizationRegistry 的回调执行时机是不确定的,可能永远不会执行,也可能在很久之后才执行。
// 不要依赖 FinalizationRegistry 进行关键业务逻辑
// 以下代码是不可靠的:
let isCleaned = false;
const registry = new FinalizationRegistry(() => {
  isCleaned = true; // 这个可能永远不会执行
});
  1. 避免内存泄漏:FinalizationRegistry 本身持有引用,需要适时取消注册。
const registry = new FinalizationRegistry(cleanup);
let obj = { data: 'test' };
let token = {};

registry.register(obj, 'held value', token);

// 当不再需要监控时,取消注册
registry.unregister(token);
obj = null;
  1. WeakRef 的最佳实践:总是检查 .deref() 的结果是否为 undefined
let weakRef = new WeakRef(someObject);

// 使用前一定要检查
let obj = weakRef.deref();
if (obj !== undefined) {
  // 安全地使用 obj
  obj.doSomething();
} else {
  // 对象已被回收,需要重新创建
  console.log('对象已被垃圾回收');
}

第七步:实际应用场景

  1. 监听 DOM 元素的生命周期
class ElementObserver {
  constructor() {
    this.registry = new FinalizationRegistry((elementId) => {
      console.log(`DOM 元素 ${elementId} 已被移除`);
    });
  }
  
  observe(element) {
    const token = { id: element.id };
    this.registry.register(element, element.id, token);
    return token;
  }
}
  1. 缓存大量数据时的内存管理
class MemorySensitiveCache {
  constructor() {
    this.cache = new Map();
    this.registry = new FinalizationRegistry((key) => {
      console.log(`缓存键 ${key} 已被回收`);
      this.cache.delete(key);
    });
  }
  
  set(key, value) {
    const weakRef = new WeakRef(value);
    this.cache.set(key, weakRef);
    
    // 注册清理,当 value 被回收时删除缓存条目
    this.registry.register(value, key);
    
    return value;
  }
  
  get(key) {
    const weakRef = this.cache.get(key);
    if (!weakRef) return undefined;
    
    const value = weakRef.deref();
    if (value === undefined) {
      this.cache.delete(key);
    }
    return value;
  }
}
  1. 管理 WebGL 或 Canvas 资源
class GraphicsResourceManager {
  constructor() {
    this.registry = new FinalizationRegistry((resourceId) => {
      console.log(`释放图形资源: ${resourceId}`);
      this.releaseGLResource(resourceId);
    });
  }
  
  createTexture(image) {
    const textureId = this.generateTextureId();
    const texture = this.createGLTexture(image, textureId);
    
    // 注册自动清理
    this.registry.register(texture, textureId);
    
    return { id: textureId, texture };
  }
}

总结

WeakRef 和 FinalizationRegistry 提供了对 JavaScript 垃圾回收机制更精细的控制能力,但它们应该谨慎使用。WeakRef 适用于那些你希望缓存但又不想阻止回收的场景,而 FinalizationRegistry 适用于需要在对象被回收时执行清理操作的场景。记住,这些是高级API,大多数日常编程中并不需要它们,但在特定的内存敏感或资源管理场景中,它们是非常有用的工具。

JavaScript 中的 WeakRef 与 FinalizationRegistry WeakRef 和 FinalizationRegistry 是 ECMAScript 2021(ES12)中引入的两个与内存管理相关的重要 API,它们允许开发者与 JavaScript 的垃圾回收机制进行更精细的交互,特别是在管理对象生命周期和清理资源时。 知识描述: 在传统的 JavaScript 中,一个对象只要被引用,垃圾回收器就不会回收它。WeakRef 允许你创建一个对对象的“弱引用”,这意味着它不会阻止该对象被垃圾回收。FinalizationRegistry 则允许你注册一个回调,当某个对象被垃圾回收时,这个回调会被执行,以便进行一些清理工作。 解题过程循序渐进讲解: 第一步:理解强引用与弱引用的区别 在 JavaScript 中,通常的变量引用是“强引用”。 只要存在至少一个强引用,对象就不会被垃圾回收。而弱引用则不同: 弱引用不会阻止垃圾回收 如果对象只被弱引用持有,它会被垃圾回收 当对象被回收后,弱引用会自动变为 undefined (或根据实现返回 undefined ) 第二步:使用 WeakRef 创建弱引用 第三步:WeakRef 的使用场景 WeakRef 主要用于缓存和监控等场景,你希望持有对某个对象的引用,但又不希望阻止它被回收。 第四步:理解 FinalizationRegistry FinalizationRegistry 允许你注册一个清理回调,当对象被垃圾回收时执行。 第五步:FinalizationRegistry 的完整使用 第六步:重要注意事项和最佳实践 垃圾回收时机不确定 :FinalizationRegistry 的回调执行时机是不确定的,可能永远不会执行,也可能在很久之后才执行。 避免内存泄漏 :FinalizationRegistry 本身持有引用,需要适时取消注册。 WeakRef 的最佳实践 :总是检查 .deref() 的结果是否为 undefined 。 第七步:实际应用场景 监听 DOM 元素的生命周期 : 缓存大量数据时的内存管理 : 管理 WebGL 或 Canvas 资源 : 总结 : WeakRef 和 FinalizationRegistry 提供了对 JavaScript 垃圾回收机制更精细的控制能力,但它们应该谨慎使用。WeakRef 适用于那些你希望缓存但又不想阻止回收的场景,而 FinalizationRegistry 适用于需要在对象被回收时执行清理操作的场景。记住,这些是高级API,大多数日常编程中并不需要它们,但在特定的内存敏感或资源管理场景中,它们是非常有用的工具。