闭包导致变量无法被 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。