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);
第六步:重要注意事项和最佳实践
- 垃圾回收时机不确定:FinalizationRegistry 的回调执行时机是不确定的,可能永远不会执行,也可能在很久之后才执行。
// 不要依赖 FinalizationRegistry 进行关键业务逻辑
// 以下代码是不可靠的:
let isCleaned = false;
const registry = new FinalizationRegistry(() => {
isCleaned = true; // 这个可能永远不会执行
});
- 避免内存泄漏:FinalizationRegistry 本身持有引用,需要适时取消注册。
const registry = new FinalizationRegistry(cleanup);
let obj = { data: 'test' };
let token = {};
registry.register(obj, 'held value', token);
// 当不再需要监控时,取消注册
registry.unregister(token);
obj = null;
- WeakRef 的最佳实践:总是检查
.deref()的结果是否为undefined。
let weakRef = new WeakRef(someObject);
// 使用前一定要检查
let obj = weakRef.deref();
if (obj !== undefined) {
// 安全地使用 obj
obj.doSomething();
} else {
// 对象已被回收,需要重新创建
console.log('对象已被垃圾回收');
}
第七步:实际应用场景
- 监听 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;
}
}
- 缓存大量数据时的内存管理:
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;
}
}
- 管理 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,大多数日常编程中并不需要它们,但在特定的内存敏感或资源管理场景中,它们是非常有用的工具。