在傳統的網頁開發時無需過多考慮記憶體管理,通常也不會產生嚴重的後果。因為當使用者點選連結開啟新頁面或者重新整理頁面,頁面內的資訊就會從記憶體中清理掉。
隨著SPA(Single Page Application)應用的增多,迫使我們在編碼時需要更多的關注記憶體。因為如果應用使用的記憶體逐漸增多會直接影響到網頁的效能,甚至導致瀏覽器標籤頁崩潰。
這篇文章,我們將研究JavaScript編碼導致記憶體洩漏的場景,提供一些記憶體管理的建議。
什麼是記憶體洩漏?
我們知道瀏覽器會把object儲存在堆記憶體中,它們通過索引鏈可以被訪問到。GC(Garbage Collector) 是一個JavaScript引擎的後臺程式,它可以鑑別哪些物件是已經處於無用的狀態,移除它們,釋放佔用的記憶體。
本該被GC回收的變數,如果被其他物件索引,而且可以通過root訪問到,這就意味著記憶體中存在了冗餘的記憶體佔用,會導致應用的效能降級,這時也就發生了記憶體洩漏。
怎樣發現記憶體洩漏?
記憶體洩漏一般不易察覺和定位,藉助瀏覽器的內建工具可以幫助我們分析是否存在記憶體洩漏,和導致記憶體洩漏的原因。
開發者工具
開啟開發者工具-Performance選項卡,可以分析當前頁面的視覺化資料。Chrome 和 Firefox 都有出色的記憶體分析工具,通過分析快照為開發者提供記憶體的分配情況。
JS導致記憶體洩漏的常見情形
未被注意的全域性變數
全域性變數可以被root訪問,不會被GC回收。一些非嚴格模式下的區域性變數可能會變成全域性變數,導致記憶體洩漏。
- 給沒有宣告的變數賦值
- 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'
如何避免?使用嚴格模式。
閉包
閉包函式執行完成後,作用域中的變數不會被回收,可能會導致記憶體洩漏:
function outer() {
const potentiallyHugeArray = [];
return function inner() {
potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
console.log('Hello');
};
};
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)
定時器
使用setTimeout 或者 setInterval:
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);
}
}
setInterval(setCallback(), 1000); // how do we stop it?
只有當定時器被清理掉的時候,它回撥函式內部的data才會被從記憶體中清理,否則在應用退出前一直會被保留。
如何避免?
function setCallback() {
// 'unpacking' the data object
let counter = 0;
const hugeString = new Array(100000).join('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 = setInterval(setCallback(), 1000); // saving the interval ID
// doing something ...
clearInterval(timerId); // stopping the timer i.e. if button pressed
定時器賦值給timerId,使用clearInterval(timerId)手動清理。
Event listeners
addEventListener 也會一直保留在記憶體中無法回收,直到我們使用了 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
});
如何避免?
function listener() {
doSomething(hugeString);
}
document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // ...and here
// 或者
document.addEventListener('keyup', function listener() {
doSomething(hugeString);
}, {once: true}); // listener will be removed after running once
JS記憶體洩漏總結
鑑別和修復JS記憶體使用問題是一項有挑戰性的任務,編碼過程中也要把避免記憶體洩漏放在第一位。因為這關係到了應用的效能和使用者體驗。
本文翻譯自 《Causes of Memory Leaks in JavaScript and How to Avoid Them》