JavaScript作为一种垃圾回收语言,通常我们不必关心对象的分配和释放问题。但偶尔,在处理回调函数时,即使不再有任何有意义的引用,也很容易让对象永远保持活跃状态。
语言为我们提供了几种工具来处理这些情况:
- WeakRef:用于存储对对象的单个弱引用
- WeakMap:只要对象存在,就将值与对象关联起来
- WeakSet:只要对象存在,就将其记住
- FinalizationRegistry:当对象被收集时执行某些操作
根据情况,我们可能需要这些功能中的一个或另一个,但我今天想描述的情况将使用第一个和最后一个功能。
一个常见的情况是对象关心某些外部状态的变化,只要它们存在就要关注。例如,自定义元素可能希望在window对象上监听"scroll"事件。但是,简单地向window添加事件侦听器意味着保留对对象的引用。如果这些自定义元素的生命周期很短但数量很多,它们将在内存中累积,并且额外的事件侦听器也会堆积并浪费处理能力。
以下是一个类似情况的简单示例:
代码语言:javascript复制class MyElement extends HTMLElement {
constructor() {
super()
window.addEventListener("scroll", event => {
this.handleScroll()
})
}
handleScroll() {
this.classList.toggle("top", window.scrollY == 0)
}
}
我们希望在对象被垃圾回收时移除事件侦听器。为了实现这一点,我们可以利用两个特性:
首先,将事件侦听器中对this的强引用替换为WeakRef将阻止事件侦听器在没有其他引用存在时保持对象活跃。一旦对象被收集,deref()方法将返回undefined。
代码语言:javascript复制const ref = new WeakRef(this)
window.addEventListener("scroll", event => {
ref.deref()?.handleScroll()
})
这将允许对象被垃圾回收,但将保留事件侦听器附加,这意味着它仍将在每个滚动事件上触发,无法解除引用并因此什么也不做。
清理事件侦听器的一种简单方法是将AbortController与FinalizationRegistry结合使用。
前者让我们向事件传递一个信号,该信号将删除事件,而后者允许我们在某些对象被收集时运行一些代码。
这个接口相对基本:我们创建一个新的FinalizationRegistry并传递一个回调。然后,我们注册一个对象A和一个关联的(不同的)对象B。当A被垃圾回收时,显然无法将其传递给回调,因此回调会传递B。
代码语言:javascript复制const abortRegistry = new FinalizationRegistry(c => c.abort())
现在,这个abortRegistry允许我们注册一个对象和一个关联的AbortController,并且每当对象被收集时,将调用controller的abort()方法。
现在我们只需要在创建时注册对象,并将控制器的信号传递给事件侦听器。
以下是完整的代码:
代码语言:javascript复制const abortRegistry = new FinalizationRegistry(c => c.abort())
class MyElement extends HTMLElement {
constructor() {
super()
const ref = new WeakRef(this)
const controller = new AbortController()
abortRegistry.register(this, controller)
window.addEventListener("scroll", event => {
ref.deref()?.handleScroll()
}, { signal: controller.signal })
}
handleScroll() {
this.classList.toggle("top", window.scrollY == 0)
}
}
我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!