前言
看到一个问题:
addEventListener在removeListener会不会造成内存泄漏?
理解addEventListener事件监听和 removeListener移除事件监听,想一想如何更好的回答这个问题, 那么从最基础的深入去学习理解一遍之后再做解答。继上一篇文章, 针对这道每日一天问题,学习了事件、事件流,事件冒泡、事件捕获。
今题来剥析内存泄漏的概念, 以及Javascript中常见的内存泄漏源。
思考理解
- 内存泄漏概念
- JS代码中常见的几个内存泄漏源
什么是内存泄漏?
概念:
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
JavaScript是一个有垃圾回收机制的语言,我们不需要手动回收内存。当本应该在垃圾回收周期中清理的内存中的对象,通过另一个对象的无意引用从根保持可以访问状态时,就会发生内存泄漏,并可能导致性能下降的后果。
JS代码中常见的几个内存泄漏源
全局变量
全局变量总是从根可用,并且永远不会回收垃圾。在非严格模式下,一些错误会导致变量从本地域泄漏到全局域:
- 将值分配给未声明的变量;
- 使用“this”指向全局对象。
function createGlobalVariables() {
leaking1 = 'I leak into the global scope'; // assigning value to the undeclared variable
this.leaking2 = 'I also leak into the global scope'; // 'this' points to the global object
};
createGlobalVariables();
window.leaking1; // 'I leak into the global scope'
window.leaking2; // 'I also leak into the global scope'
预防措施:使用严格模式(“use strict”)。
闭包
函数作用域内的变量将在函数推出调用栈后清楚,并且如果函数外部没有其他指向它们的引用,则将清理它们。但闭包将保留引用的变量并保持活动状态。
function outer(){
const potentiallyHugeArray = [];
return function inner(){
// 闭包
potentiallyHugeArray.push('hello'); //function inner is closed over the potentiallyHugeArray variable
};
};
const sayHello = outer(); //contains definition of the function inner
function repeat(fn, num){
for(let i=0; i< num; i++){
fn();
}
}
repeat(sayHello,10);// each sayHello call pushes another 'Hello' to the potentiallyHugeArray
// now imagine repeat(sayHello, 100000)
在上例子中, 从任何一个函数都不会返回 potentialHugeArray,并且无法到达它,但它的大小可以无限增加,具体取决于我们调用函数 inner()的次数。
预防措施: 闭包是肯定会用到的,所以重要的是:
- 了解何时创建了闭包,以及它保留了哪些对象;
- 了解闭包的预期寿命和用法(尤其是用作回调时)。
计时器
如果我们在代码中设置了递归计时器(recurring timer),则只要回调可调用,计时器回调中对该对象的引用就将保持活动状态。
在下面的示例中,由于我们没有对 setInterval 的引用,因此它永远不会被清除,并且 data.hugeString 会一直保留在内存中。
function setCallback(){
const data = {
counter: 0,
hugeString: new Array(100000).join('x')
};
return function cb(){
data.counter++; //data object is now part of the callback's scope
console.log(data.counter)
}
}
setIntervel(setCallback(),1000); // how do we stop it?
预防措施: 尤其是在回调的生命周期不确定或undefined的情况下:
- 了解从计时器的回调中引用了哪些对象;
- 使用计时器返回的句柄在必要时取消它。
function setCallbakc(){
//unpacking the data object
let counter = 0;
const hugeString = new Array(100000).jion('x'); // gets removed when the setCallback returns
return function cb() {
counter++: // only counter is part of the callback's scope
console.log(counter);
}
}
const timerId = setIntervel(setCallback(),1000);// saving the interval ID
// doing something
clearIntervel(timerId); //stopping the timer i.e if button pressed
事件侦听器
添加后,事件侦听器将一直保持有效,直到:
- 使用 removeEventListener()显示删除它。
- 关联的DOM元素被移除
对于某些类型的事件,应该是一直保留到用户离开页面为止。但是,有时我们希望事件侦听器执行特定的次数。
const hugeString .= new Array(100000).join('x');
document.addEventListener('keyup',function(){// anonymous inline function - can't remove it
doSomething(hugeString); //hugeString is now forever kept in the callback's scope
});
在上面的示例中,用一个匿名内联函数作为事件侦听器,这意味着无法使用 removeEventListener() 将其删除。同样,该文档也无法删除,因此即使我们只需要触发它一次,它和它域中的内容就都删不掉了。
预防措施: 我们应该始终创建指向事件侦听器的引用并将其传递给 removeEventListener(),来注销不再需要的事件侦听器。
function listener(){
doSomething(hugeString);
}
document.addEventListener('keyup',listener); //named function can be referenced here...
document.removeEventlistener('keyup',listener); // ...and here
如果事件侦听器仅执行一次,则addEventListener()可以使用第三个参数。假设{once: true} 作为第三个参数传递给 addEventListener(),则在处理一次事件后, 将自动删除侦听器函数。
document.addEventListener('keyup', function listener() {
doSomething(hugeString);
}, {once: true}); // listener will be removed after running once
缓存
如果我们不删除未使用的对象且不控制对象大小,那么缓存就会失控。
let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const mapCache = new Map();
function cache(obj){
if (!mapCache.has(obj)){
const value = `${obj.name} has an id of ${obj.id}`;
mapCache.set(obj, value);
return [value, 'computed'];
}
return [mapCache.get(obj), 'cached'];
}
cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_1); // ['Peter has an id of 12345', 'cached']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321")
user_1 = null; // removing the inactive user
// Garbage Collector
console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321") // first entry is still in cache
在上面的示例中,缓存仍保留在 user_1 对象上。因此,我们还需要清除不会再重用的条目的缓存。
可能的解决方案:我们可以使用 WeakMap。它的数据结构中,键名是对象的弱引用,它仅接受对象作为键名,所以其对应的对象可能会被自动回收。当对象被回收后,WeakMap 自动移除对应的键值对。在以下示例中,在使 user_1 对象为空后,下一次垃圾回收后关联的条目会自动从 WeakMap 中删除。
let user_1 = { name: "Peter", id: 12345 };
let user_2 = { name: "Mark", id: 54321 };
const weakMapCache = new WeakMap();
function cache(obj){
// ...same as above, but with weakMapCache
return [weakMapCache.get(obj), 'cached'];
}
cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(weakMapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321"}
user_1 = null; // removing the inactive user
// Garbage Collector
console.log(weakMapCache); // ((…) => "Mark has an id of 54321") - first entry gets garbage collected
分离的DOM元素
如果 DOM 节点具有来自 JavaScript 的直接引用,则即使从 DOM 树中删除了该节点,也不会对其垃圾回收。
在以下示例中,我们创建了一个 div 元素并将其附加到 document.body。removeChild() 无法正常工作,并且由于仍然存在指向 div 的变量,所以堆快照将显示分离的 HTMLDivElement。
function createElement() {
const div = document.createElement('div');
div.id = 'detached';
return div;
}
// this will keep referencing the DOM element even after deleteElement() is called
const detachedDiv = createElement();
document.body.appendChild(detachedDiv);
function deleteElement() {
document.body.removeChild(document.getElementById('detached'));
}
deleteElement(); // Heap snapshot will show detached div#detached
怎么预防呢?一种方案是将 DOM 引用移入本地域。在下面的示例中,在函数 appendElement() 完成之后,将删除指向 DOM 元素的变量。
function createElement() {...} // same as above
// DOM references are inside the function scope
function appendElement() {
const detachedDiv = createElement();
document.body.appendChild(detachedDiv);
}
appendElement();
function deleteElement() {
document.body.removeChild(document.getElementById('detached'));
}
deleteElement(); // no detached div#detached elements in the Heap Snapshot
参考:
https://www.ditdot.hr/en/causes-of-memory-leaks-in-javascript-and-how-to-avoid-them