闭包导致变量无法被 GC(垃圾回收),这是早就知道的事情,所以平时也是尽量不用闭包,加上一般业务场景也不会有很高的内存占用和使用闭包的需求,所以也一直没怎么碰到相关问题。
现在写这篇文章是因为这两天碰到了。
场景是这样的,一个函数对图片的 ImageData 进行处理,结束后返回新的 ImageData,并且处理过程可以中断,所以函数运行时会先设置一个停止函数在函数外,用于下次调用的时候先停止之前的任务。这就形成了一个闭包环境。中途产生的 ImageData 就无法被回收。
// 示例代码
export default class FilterManager {
filters: Filter[] = []
source?: ImageData
private taskStopHandler?: () => void
// ……
async applyFilters() {
if (this.taskStopHandler) {
this.taskStopHandler()
}
let stopFlag = false
// 停止函数
this.taskStopHandler = () => {
stopFlag = true
this.taskStopHandler = undefined
}
let result: ImageData | undefined = this.source
const filters = [...this.filters]
const cb = callback || this.callback
// 计算滤镜
for (let i = index; i < filters.length; i++) {
if (stopFlag) break
if (result) {
result = await filters[i].processor(result, filters[i].params)
}
}
// 回调返回必须有结果
if (!stopFlag && result) {
if (typeof cb === 'function') {
cb(result)
}
} else {
this.status = StatusEnum.NoData
}
// 释放闭包
// this.taskStopHandler?.()
return result
}
}
这里的 taskStopHandler 如果不及时调用就会导致 applyFilters 内部变量无法被及时回收。如果是一般业务还无所谓,但这个是图片处理,一个五千万像素图片的 ImageData 就要 200M 内存,而现在很多手机拍照开启完整像素是超过五千万像素的。

上图不是很清晰,但可以从右侧看到这个两百多兆的 ImageData 和 taskStopHandler 造成的闭包有关。当我将上面代码里最后两行注释取消,即函数最后调用一下 taskStopHandler,释放闭包内变量之后,内存占用就如图中左侧二三行所示,少了两百多兆。至于为什么还有三百兆,那是因为原图像数据存在 filterManager 的 source 上。应该……也可以干掉,虽然这样会导致每次处理的时候都需要重新获取 ImageData。