<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
大家好,我是CUGGZ。SPA(單頁應用程式)的興起,促使我們更加關注與記憶體相關的 JavaScript 編碼實踐。如果應用使用的記憶體越來越多,就會嚴重影響效能,甚至導致瀏覽器的崩潰。下面就來看看JavaScript中常見的記憶體漏失以及如何避免記憶體漏失。
JavaScript 就是所謂的垃圾回收語言之一,垃圾回收語言通過定期檢查哪些先前分配的記憶體仍然可以從應用程式的其他部分“存取”來幫助開發人員管理記憶體。垃圾回收語言中洩漏的主要原因是不需要的參照。如果你的 JavaScript 應用程式經常發生崩潰、高延遲和效能差,那麼一個潛在的原因可能是記憶體漏失。
在 JavaScript 中,記憶體是有生命週期的:
在JavaScript中,物件會儲存在堆記憶體中,可以根據參照鏈從根存取它們。垃圾收集器是 JavaScript 引擎中的一個後臺程序,用於識別無法存取的物件、刪除它們並回收記憶體。
下面是垃圾收集器根到物件的參照鏈範例:
當記憶體中應該在垃圾回收週期中清理的物件,通過另一個物件的無意參照從根保持可存取時,就會發生記憶體漏失。將冗餘物件保留在記憶體中會導致應用程式內部使用過多的記憶體,並可能導致效能下降。
那該如何判斷程式碼正在洩漏記憶體呢?通常,記憶體漏失是很難被發現的,並且瀏覽器在執行它時不會丟擲任何錯誤。如果注意到頁面的效能越來越差,瀏覽器的內建工具可以幫助我們確定是否存在記憶體漏失以及導致記憶體漏失的物件。
記憶體使用檢查最快的方法就是檢視瀏覽器的工作管理員。 它們提供了當前在瀏覽器中執行的所有索引標籤和程序的概覽。在工作管理員中檢視每個索引標籤的 JavaScript 記憶體佔用情況。如果網站什麼都不做,但是 JavaScript 記憶體使用量卻在逐漸增加,那麼很有可能發生了記憶體漏失。
我們可以通過了解在 JavaScript 中如何建立不需要的參照來防止記憶體漏失。以下情況就會導致不需要的參照。
全域性變數始終可以從全域性物件(在瀏覽器中,全域性物件是window)中獲得,並且永遠不會被垃圾回收。在非嚴格模式下,以下行為會導致變數從區域性範圍洩露到全域性範圍:
(1)為未宣告的變數賦值
這裡我們給函數中一個未宣告的變數bar賦值,這時就會使bar成為一個全域性變數:
function foo(arg) { bar = "hello world"; }
這就等價於:
function foo(arg) { window.bar = "hello world"; }
這樣就會建立一個多餘的全域性變數,當執行完foo函數之後,變數bar仍然會存在於全域性物件中:
foo() window.bar // hello world
(2)使用指向全域性物件的 this
使用以下方式也會建立一個以外的全域性變數:
function foo() { this.bar = "hello world"; } foo();
這裡foo是在全域性物件中呼叫的,所以其this是指向全域性物件的(這裡是window):
window.bar // hello world
我們可以通過使用嚴格模式“use strict”來避免這一切。在JavaScript檔案的開頭,它將開啟更嚴格的JavaScript解析模式,從而防止意外的建立全域性變數。
需要特別注意那些用於臨時儲存和處理大量資訊的全域性變數。如果必須使用全域性變數儲存資料,就使用全域性變數儲存資料,但在不再使用時,就手動將其設定為 null,或者在處理完後重新分配。否則的話,請儘可能的使用區域性變數。
使用 setTimeout 或 setInterval 參照回撥中的某個物件是防止物件被垃圾收集的最常見方法。如果我們在程式碼中設定了迴圈計時器,只要回撥是可呼叫的,計時器回撥中對物件的參照就會保持活動狀態。
在下面的範例中,只有在清除計時器後,才能對資料物件進行垃圾收集。由於我們沒有對setInterval的參照,所以它永遠無法被清除和刪除資料。hugeString會一直儲存在記憶體中,直到應用程式停止,儘管從未使用過。
function setCallback() { const data = { counter: 0, hugeString: new Array(100000).join('x') }; return function cb() { data.counter++; // data物件是回撥範圍的一部分 console.log(data.counter); } } setInterval(setCallback(), 1000);
當執行這段程式碼時,就會每秒輸出一個數位:
那我們如何去阻止他呢?尤其是在回撥的壽命未定義或不確定的情況下:
function setCallback() { // 將資料物件解包 let counter = 0; const hugeString = new Array(100000).join('x'); // 在setCallback返回時被刪除 return function cb() { counter++; // 只有計數器counter是回撥範圍的一部分 console.log(counter); } } const timerId = setInterval(setCallback(), 1000); // 儲存定時器的ID // 合適的時機清除定時器 clearInterval(timerId);
我們知道,函數範圍內的變數在函數退出呼叫堆疊後,如果函數外部沒有任何指向它們的參照,則會被清除。儘管函數已經完成執行,其執行上下文和變數環境早已消失,但閉包將保持變數的參照和活動狀態。
function outer() { const potentiallyHugeArray = []; return function inner() { potentiallyHugeArray.push('Hello'); console.log('Hello'); }; }; const sayHello = outer(); function repeat(fn, num) { for (let i = 0; i < num; i++){ fn(); } } repeat(sayHello, 10);
顯而易見,這裡就形成了一個閉包。其輸出結果如下:
這裡,potentiallyHugeArray 永遠不會從任何函數返回,也無法存取,但它的大小可能會無限增長,這取決於呼叫函數 inner() 的次數。
那該如何防止這個問題呢?閉包是不可避免的,也是JavaScript不可或缺的一部分,因此重要的是:
活動事件偵聽器將防止在其範圍內捕獲的所有變數被垃圾收集。新增後,事件偵聽器將一直有效,直到:
對於某些型別的事件,它會一直保留到使用者離開頁面,就像應該多次單擊的按鈕一樣。但是,有時我們希望事件偵聽器執行一定次數。
const hugeString = new Array(100000).join('x'); document.addEventListener('keyup', function() { // 匿名行內函式,無法刪除它 doSomething(hugeString); // hugeString 將永遠保留在回撥的範圍內 });
在上面的範例中,匿名行內函式用作事件偵聽器,這意味著不能使用 removeEventListener() 刪除它。同樣,document 不能被刪除,因此只能使用 listener 函數以及它在其範圍內保留的內容,即使只需要啟動一次。
那該如何防止這個問題呢?一旦不再需要,我們應該通過建立指向事件偵聽器的參照並將其傳遞給 removeEventListener() 來登出事件偵聽器。
function listener() { doSomething(hugeString); } document.addEventListener('keyup', listener); document.removeEventListener('keyup', listener);
如果事件偵聽器只能執行一次,addEventListener() 可以接受第三個引數,這是一個提供附加選項的物件。假定將 {once:true} 作為第三個引數傳遞給 addEventListener() ,則偵聽器函數將在處理一次事件後自動刪除。
document.addEventListener('keyup', function listener() { doSomething(hugeString); }, {once: true});
如果我們不斷地將記憶體新增到快取中,而不刪除未使用的物件,並且沒有一些限制大小的邏輯,那麼快取可以無限增長。
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; console.log(mapCache); // {{…} => 'Peter has an id of 12345', {…} => 'Mark has an id of 54321'}
在上面的範例中,快取仍然保留 user_1 物件。因此,我們需要將那些永遠不會被重用的變數從快取中清除。
可以使用 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){ // ... 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; console.log(weakMapCache); // {(…) => "Mark has an id of 54321"}
如果DOM節點具有來自 JavaScript 的直接參照,它將防止對其進行垃圾收集,即使在從DOM樹中刪除該節點之後也是如此。
在下面的範例中,建立了一個div元素並將其附加到 document.body 中。removeChild() 就無法按預期工作,堆快照將顯示分離的HTMLDivElement,因為仍有一個變數指向div。
function createElement() { const div = document.createElement('div'); div.id = 'detached'; return div; } // 即使在呼叫deleteElement() 之後,它仍將繼續參照DOM元素 const detachedDiv = createElement(); document.body.appendChild(detachedDiv); function deleteElement() { document.body.removeChild(document.getElementById('detached')); } deleteElement();
要解決此問題,可以將DOM參照移動到本地範圍。在下面的範例中,在函數appendElement() 完成後,將刪除指向DOM元素的變數。
function createElement() {...} // DOM參照在函數範圍內 function appendElement() { const detachedDiv = createElement(); document.body.appendChild(detachedDiv); } appendElement(); function deleteElement() { document.body.removeChild(document.getElementById('detached')); } deleteElement();
偵錯記憶體問題是一項複雜的工作,我們可以使用 Chrome DevTools 來識別記憶體圖和一些記憶體漏失,我們需要關注以下兩個方面:
以下面的程式碼為例,有兩個按鈕:列印和清除。點選“列印”按鈕,通過建立 paragraph 節點並將大字串設定到全域性,將1到10000的數位追加到DOM中。
“清除”按鈕會清除全域性變數並覆蓋 body 的正文,但不會刪除單擊“列印”時建立的節點:
<!DOCTYPE html> <html lang="en"> <head> <title>Memory leaks</title> </head> <body> <button id="print">列印</button> <button id="clear">清除</button> </body> </html> <script> var longArray = []; function print() { for (var i = 0; i < 10000; i++) { let paragraph = document.createElement("p"); paragraph.innerHTML = i; document.body.appendChild(paragraph); } longArray.push(new Array(1000000).join("y")); } document.getElementById("print").addEventListener("click", print); document.getElementById("clear").addEventListener("click", () => { window.longArray = null; document.body.innerHTML = "Cleared"; }); </script>
當每次點選列印按鈕時,JavaScript Heap都會出現藍色的峰值,並逐漸增加,這是因為JavaScript正在建立DOM節點並字串新增到全域性陣列。當點選清除按鈕時,JavaScript Heap就變得正常了。除此之外,可以看到節點的數量(綠色的線)一直在增加,因為我們並沒有刪除這些節點。
在實際的場景中,如果觀察到記憶體持續出現峰值,並且記憶體消耗一直沒有減少,那可能存在記憶體洩露。
當一個節點從 DOM 樹中移除時,它被稱為分離,但一些 JavaScript 程式碼仍然在參照它。讓我們使用下面的程式碼片段檢查分離的 DOM 節點。通過單擊按鈕,可以將列表元素新增到其父級中並將父級分配給全域性變數。簡單來說,全域性變數儲存著 DOM 參照:
var detachedElement; function createList(){ let ul = document.createElement("ul"); for(let i = 0; i < 5; i++){ ul.appendChild(document.createElement("li")); } detachedElement = ul; } document.getElementById("createList").addEventListener("click", createList);
我們可以使用 heap snapshot 來檢查分離的DOM節點,可以在Chrome DevTools 的Memory面板中開啟Heap snapshots選項:
點選頁面的按鈕後,點選下面藍色的Take snapshot按鈕,我們可以在中間的搜尋欄目輸入Detached來過濾結果以找到分離的DOM節點,如下所示:
當然也可以嘗試使用此方法來識別其他記憶體漏失。
到此這篇關於一文搞懂如何避免JavaScript記憶體漏失的文章就介紹到這了,更多相關JavaScript記憶體漏失內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45