别再沉溺于那段回忆了🛑
我从事 Web 应用程序开发已经十多年了。从经典的 ASP到PHP,再到ASP.NET Web 表单等等,不胜枚举。然而,这么多年来,我始终关注网站的性能。其中一项重要的工作就是防范内存泄漏,因为内存泄漏会导致页面加载速度极慢,严重时甚至会导致网站崩溃。
引言
内存泄漏是软件开发中常见的问题,无论你使用的语言是否是内存管理型语言(即带有垃圾回收机制的语言)。内存泄漏是指分配了一块内存,但应用程序从未释放这块内存,导致它没有返回给容器应用程序或操作系统。
我记得大学时学过这个概念,但除了通常会有一个由所有已占用内存位置组成的树状结构之外,其他什么都记不起来了。每次垃圾回收器检查内存时,都会解析这棵树,如果某个节点没有连接到任何分支,它就会被回收并返回给主程序。
大多数网页开发者可能会使用主流框架或库来编写应用程序。有些人可能会使用像 PHP 或 Ruby 这样比较老的语言,但无论我们使用什么,都很有可能以某种方式遇到这个问题。
结果
那么,当我们的应用程序出现内存泄漏时会发生什么呢🤔?
在某些情况下,内存占用会持续上升。如果用户使用的是配置不错的电脑,他们可能根本不会注意到这一点。并非所有人都像我们开发人员一样,会经常查看任务管理器来了解内存使用情况。
无论如何,这会减慢页面加载速度,使交互无响应,甚至可能导致标签页或整个窗口崩溃。
JavaScript 内存泄漏
在 JavaScript 中,分配内存然后置之不理非常容易。即使你编写的不是纯 JavaScript 代码,仍然可能发生内存泄漏,而你却浑然不觉。
但这究竟是如何发生的呢?
在 JavaScript 中,内存泄漏可能通过以下几种方式发生。
- 无意中创建了全局变量
- 定时器和回调函数
- DOM 引用之外
- 关闭
- 事件监听器
全局变量
在 JavaScript 中创建不需要的全局变量非常简单。请看下面的代码:
function helloWorld() {
name = 'Yas';
console.log(`Hello ${name}`);
}
在这个简单的函数中,我们创建了一个名为 name 的全局变量。我们原本并不想这样做,但最终还是这么做了。
💡 引用未声明的变量会在全局对象内部创建一个新变量。
如果你使用以下方式,也会发生同样的情况this:
function helloWorld(args) {
this.name = 'Yas';
console.log(`Hello ${name}`);
}
❗ 为防止此类泄漏,请使用 JavaScript 的启用模式。您可以通过在 JavaScript 文件顶部
strict添加以下代码来实现。use strinct;
尽管我们将意外产生的全局变量视为内存泄漏的来源之一,但我们使用的框架中仍然定义着许多全局变量,甚至包括我们有意创建的全局变量。请记住,这些变量是不可回收的,除非被置为空或重新赋值,否则垃圾回收器无法对其进行任何处理。
定时器和回调函数
setInternal随着我们转向更现代的概念(例如 Observable), `and`的使用setTimeout越来越少见async/await。此外,一些库和框架提供可观察对象来简化回调。在这种情况下,它们有责任确保在自身实例销毁后,回调函数将无法访问。
然而,在很多情况下,我们需要使用它来稍后或按计划调用函数。
let data = fetchData();
setInternal(function() {
let node = document.querySelector('#list');
// loop through data and create the html
node.innerHTML = transform(data);
}, 1000)
这个例子展示了定时器如何变成无法回收的对象。即使列表节点从 DOM 中移除,处理程序内部的引用仍然有效且无法回收。这导致其依赖项也无法回收。因此,数据变量(可能非常大)会在不再需要后长时间占用内存。
现在我们来看看如何改进这段代码以避免内存泄漏:
let node = document.querySelector('#list');
let data = fetchData();
function handler(data) {
if(node) {
// do stuff with data and create the list
node.innerHTML = transform(data);
}
};
setInterval(handler, 1000);
DOM 引用已失效(分离的 DOM)
当某些节点从 DOM 中移除,但仍通过 JavaScript 保留在内存中时,就会出现 DOM 引用溢出或 DOM 分离的情况。通常这意味着存在一个指向该节点的变量的引用。
DOM 是一棵双向链树,这意味着对任何节点的任何引用都会导致整棵树不会被垃圾回收。
我们来看一个例子,以便更清楚地说明这一点:
function create() {
let ul = document.createElement('ul');
ul.id = 'list';
for (var i = 0; i < 10; i++) {
var li = document.createElement('li');
li.textContent = `Item # ${i}`;
ul.appendChild(li);
}
return ul;
}
const list = create();
document.body.appendChild(list);
function deleteList() {
document.body.removeChild(document.getElementById('list'));
}
document.getElementById('delete').addEventListener('click', deleteList);
点击删除按钮会将列表从 DOM 中移除,但由于 JavaScript 中存在引用,该列表永远不会被垃圾回收。我们可以使用浏览器开发者工具中的堆快照来识别分离的节点。我这里使用的是 Chrome 浏览器,但您也可以使用 Edge(与 Chrome 类似)或Firefox。
拍摄快照后,在筛选文本框中输入“detached”,即可看到分离的 DOM 节点。
解决这类问题的办法是始终使用局部变量,以便在函数执行完毕后销毁引用。
关闭
闭包是 JavaScript 的一个特性,也是大多数初学者感到棘手的地方。但一旦掌握了它,就会发现它其实很容易理解。闭包的核心思想是允许你从内部函数访问外部函数的作用域。
更专业的定义是,闭包是将一个函数与其周围状态(词法环境)的引用捆绑在一起的组合。
function init() {
var hello = 'Hello'; // hello is a local variable created by init
function helloWorld() { // helloWorld() is the inner function, a closure
console.log(`${hello} world!`); // use variable declared in the parent function
}
helloWorld();
}
init();
既然我们已经了解了什么是闭包,接下来让我们看看它们是如何导致内存泄漏的。假设有以下代码:
var newElem;
function outer() {
var someText = new Array(1000000);
var elem = newElem;
function inner() {
if (elem) return someText;
}
return function () {};
}
setInterval(function () {
newElem = outer();
}, 5);
在上面的代码中,该inner函数从未被调用,但它持有对 `x` 的引用。请记住,内部 `x` 的作用域与外部函数返回的 `x`elem的作用域相同。由于`x` 是一个全局变量,只要有引用指向`x`,共享上下文就会被保留。每次调用都会导致一个残留引用,随着时间的推移,最终会耗尽内存。function () {}newElemfunction () {}someText
那么我们该如何应对这种情况呢?首先,我们需要停止使用 ` var.`。此外,如果我们像这样调用内部函数outer()(),那么就不会留下任何引用了。
事件监听器
每次将事件处理程序附加到特定元素时,都需要保留一个引用,并在完成后将其移除。因此,不要这样做:
function deleteList() {}
document.getElementById('delete').addEventListener('click', deleteList);
我们应该这样做:
function deleteList() {}
document.getElementById('delete').addEventListener('click', deleteList);
// do stuff
document.getElementById('delete').removeEventListener('click', deleteList);
概括
我们已经了解了 JavaScript 中可能导致内存泄漏的原因以及如何修复这些问题。但是,请记住,在大多数情况下,如果您使用的是框架或库,这些问题都会自动处理。如果您使用的某个库可能存在内存泄漏,您可以使用浏览器开发者工具的内存分析器轻松找到原因。
希望这篇文章能提高大家的意识,让你们编写出性能更高的代码,从而显著提升用户体验。谁也不想让浏览器像吃芝士蛋糕一样吞噬内存,对吧😁?
资源
- 关于Chrome开发者工具内存分析器的更多信息。
- 了解Firefox DevTools 内存分析功能。


