【好文翻译】JavaScript 中的内存泄漏
如果你也受到JS内存泄漏困扰,或者对WeakMap/WeakSet等数据结构的使用场景有疑惑,不妨来看看@徐姣同学翻译的这篇文章——JavaScript 中的内存泄漏。原文地址:medium.com/javascript-…
JavaScript 被认为是一种高级编程语言,与 C 或 C++ 等低级编程语言相比(注:高级编程语言指的是语法趋近于人类的自然语言,低级编程语言指的则是语法趋近于汇编语言,语言之间本无高级、低级之分),开发人员不需要显式分配和释放内存,垃圾回收器会帮我们做这部分的事情。乍一看,这好像是一件好事,因为我们可以专注于实现业务功能,而不需要花费额外的精力去关注内存管理相关的事情。但是,这也要求我们熟悉对象可达性(objects reachability)的概念,以防造成内存泄漏。
垃圾回收器通过一些标志
去识别哪些对象可以继续存在于内存中,标记清除算法
是用来给应用程序中的对象打标志。
简单的来说,如果一个对象从根对象(如浏览器环境中的window对象)开始遍历,如果是可达内存打上标记,其余的被当作垃圾回收 。为了防止内存泄漏,当应用程序不再需要某个指定对象时,我们需要使其不可访问(注意,这里不完全等同于删除某个对象所有的引用)。
虽然听起来很简单,但是我们日常开发中容易忽略一些隐式的引用,它会阻止该对象从内存中移除而引发内存泄漏。在这篇博文中,我将介绍 JavaScript 和 Angular 应用程序中最常见的内存泄漏源。
根对象(window)中的引用
假设我们创建了一个对象并使用一个变量mustang引用它 ,并且将它push到了一个全局的数组中
在chrome浏览器中打开,F12打开控制台选择Memory(内存) 页签。选择Heap snapshot(堆快照),点击左上角录制按钮
当前堆快照如下所示:
和我们预期的一样,可以通过window对象下的 mustang 变量和 cars 数组来访问这个新建的Car实例对象。
如果我们不再需要这个实例对象,我们通常的做法是将 mustang 变量设置为null,以为这样就能万事大吉了,事实上,垃圾回收仍然将认为其为可达对象,因为通过cars 数组仍可以访问到该对象。
即使 mustang 变量不再引用该对象,它仍然不能从内存中删除。
正确的解决方案是不仅要将mustang变量设置为null,同时要从cars数组中删除对该对象的引用。
在Chrome开发者工具选择内存tab页签,选择对比模式,在对比模式下可以比较表明的看到这个Car对象已成功从内存中删除
总而言之,永远不要忘记删除所有对根对象(如浏览器环境中的window对象)下对象的引用。否则将会造成内存泄漏。
事件监听器
让我们通过一个包装类给一个HTML的按钮元素添加一个点击事件
当前堆快照如下所示:
和预期的一样,该Button实例对象可通过 myBtn 变量访问,并且由于闭包该Button实例对象也同时被包含回调函数的数组引用。
将 myBtn 变量设置为 null 并不能使其从内存中移除,因为它仍然被单击处理程序的数组引用。
如果相关联的HTML元素仍然在DOM树中,在将 myBtn 变量设置为 null 之前删除事件监听器是至关重要的。
我们重新来修改 Button 类定义
现在如果我们不再需要 myBtn 对象,我们可以通过以下方式告知垃圾回收器:
打开内存页签中的对比模式,可以看到该对象已成功从内存中删除
如果相应的 HTML 元素在包装器销毁时同时被销毁,那我们就不需要手动去删除事件侦听器,因为该对象已经是不可达对象。
但这也只是在以下场景生效,如果你有一个组件作为一些自包含的HTML代码的包装类,我们不需要花精力去关注我们是否显示的删除了对应DOM注册所有事件侦听器,因为这些DOM元素的生命周期和这个组件的生命周期是一致的,会伴随这组件的销毁而销毁。
集合
如果我们在某个集合中存储了对某个对象的引用,那么该对象将是可访问的除非它被显式地从集合中删除或集合不再可访问:
其堆快照如下所示:
显然,该对象可通过 mustang 变量访问,并由 Array、Set 和 Map 实例引用。
因此,将变量设置为 null 并不能从内存中删除该对象:
为了防止内存泄漏,除了将变量设置为null外,我们还需要将对象从每个集合中删除,或者所有集合都必须变得不可访问:
或者。我们还可以使用 WeakSet 和 WeakMap 替代方案,它们不需要显式删除对象:
如果对象不再可以通过集合之外的其他方式访问,集合中的对象和相应的内容可以从内存中删除。我们可以看到,将 mustang 变量设置为 null 后其堆快照比较如下所示:
Angular 组件——事件监听器
如果我们直接地将事件侦听器添加到Angular组件的模板中,则无需显式删除事件侦听器,DOM 节点在其宿主组件被移除时被销毁,因此不会引入内存泄漏。
如果目标元素的生命周期超过了组件的生命周期,情况就不同了:
我们需要注意的是,如果在已注册的事件侦听器中引用该组件,该组件将被保留在内存中,否则组件将被垃圾回收,因为事件侦听器将保持注册和活动状态。所以,挂起匿名回调函数的形式可能会由于闭包而引起一个小的内存泄漏,这仍然是一个问题。
Angular 组件——可观察对象
Angular 应用程序中最常见的内存泄漏源是可观察对象。我们经常会被问一个问题,是否需要始终取消订阅 Observable。答案是否定的,技术上并不总是需要。 假设我们有一个组件级别的service,并允许子组件之间进行通信:
在上面的示例中,如果不取消订阅源也不会造成内存泄漏。 但是如果该本地服务使用了全局的单例service提供的数据流,情况就不同了:
因此,“可以不取消组件级别service提供的可观察对象的订阅”——这种说法是不安全的 这取决于该服务是否像上面的代码片段那样在内部订阅全局流。
显然,如果直接在组件中订阅全局service中的observable而没有取消订阅的话就会成内存泄漏:
最后但是也同样非常重要的一点是,当我们订阅在组件中创建的可观察对象时,会有一些场景:它是在组件级别定义的,但它被全局根对象隐式引用,这种场景下我们也需要取消订阅以防止内存泄漏, 尽管乍一看好像没有必要:
尽管 JavaScript 不需要明确的内存管理,但我们仍然需要遵循某些规则以防止内存泄漏。
Chrome DevTools 中的 Memory 和 Performance 选项卡可让您非常轻松地分析应用程序的内存状态。
毫无疑问,了解对象的可达性概念是值得的,但是大多数时候自己手动清理是安全的(就像在现实生活中😃),即删除事件监听器并取消订阅可观察对象。正如他们所说,安全总比遗憾好。
如果你喜欢这篇博文,请给我一些掌声👏
完。
转载自:https://juejin.cn/post/7249179953358602300