自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

JavaScript 內(nèi)存泄漏防范之道

開發(fā) 前端
一般情況下,忽視內(nèi)存管理不會(huì)對(duì)傳統(tǒng)的網(wǎng)頁產(chǎn)生顯著的后果。這是因?yàn)椋脩羲⑿马撁婧螅瑑?nèi)存數(shù)據(jù)都被清理了。

 一般情況下,忽視內(nèi)存管理不會(huì)對(duì)傳統(tǒng)的網(wǎng)頁產(chǎn)生顯著的后果。這是因?yàn)?,用戶刷新頁面后,?nèi)存數(shù)據(jù)都被清理了。

[[329247]]

但是隨著SPA(單頁應(yīng)用)的普及,我們不得不更加關(guān)注頁面的內(nèi)存管理。用戶在 SPA 上往往很少刷新頁面,隨著頁面停留時(shí)間的增長,內(nèi)存可能越占越多,輕則影響頁面性能,嚴(yán)重的可能導(dǎo)致標(biāo)簽頁崩潰。

在這篇文章中,我們將探討導(dǎo)致 JavaScript 中內(nèi)存泄露的常見原因,以及如何改善內(nèi)存管理。

瀏覽器將對(duì)象保留在堆內(nèi)存中,通過引用鏈可從根對(duì)象到達(dá)這些對(duì)象。垃圾回收器(GC)是 JavaScript 引擎中的一個(gè)后臺(tái)進(jìn)程,它可以識(shí)別無法到達(dá)的對(duì)象,將其刪除,并回收相應(yīng)的內(nèi)存。

 

引用鏈 - GC - 對(duì)象關(guān)系圖

當(dāng)內(nèi)存中本應(yīng)在垃圾回收循環(huán)中被清理的對(duì)象,通過另一個(gè)對(duì)象意外的引用從而維持可訪問狀態(tài),就會(huì)發(fā)生內(nèi)存泄漏。將多余的對(duì)象保持在內(nèi)存中,會(huì)導(dǎo)致應(yīng)用程序內(nèi)部的內(nèi)存使用量過大,進(jìn)而影響性能。

 

內(nèi)存泄露

如何判斷代碼是否存在內(nèi)存泄漏呢?內(nèi)存泄漏通常比較隱蔽,難以發(fā)現(xiàn)和定位。造成內(nèi)存泄漏的 JavaScript 代碼看上去挺正常,瀏覽器在運(yùn)行的時(shí)候也不會(huì)拋出錯(cuò)誤。如果發(fā)現(xiàn)頁面性能越來越差,通常是內(nèi)存泄漏的征兆,可以通過瀏覽器內(nèi)置的工具判斷是否存在內(nèi)存泄漏,并分析出原因。

最快的方法是查看瀏覽器的任務(wù)管理器(注意,不是操作系統(tǒng)的任務(wù)管理器)。它提供了瀏覽器運(yùn)行中的所有 tab 頁和進(jìn)程的資源使用情況,比如內(nèi)存占用、CPU 占用和進(jìn)程 ID 等。Chrome 的任務(wù)管理器可通過 Shift+Esc 快捷鍵打開,F(xiàn)irefox 可在地址欄輸入about:performance打開。

如果頁面都沒有任何交互,內(nèi)存占用卻越來越多,很可能存在泄漏。

  

Chrome 任務(wù)管理器

瀏覽器 DevTools 則提供了更豐富的內(nèi)存管理功能。可以在 Chrome 的性能面板錄制頁面運(yùn)行情況,查看可視化的性能分析數(shù)據(jù)。

 

Chrome 性能面板

除此之外,Chrome 和 Firefox 的 DevTools 還有專門的內(nèi)存工具用于分析內(nèi)存使用情況。通過比較連續(xù)的內(nèi)存快照,可以看出內(nèi)存分配情況。

通過前面的分析,內(nèi)存泄露的根本原因就是代碼在無意之中引用了本該被 GC 回收的對(duì)象。那么,哪些情況容易造成內(nèi)存泄露呢?

1、意外的全局變量

全局變量一直處于可訪問狀態(tài),不會(huì)被 GC 回收。在非嚴(yán)格模式下,有時(shí)會(huì)不小心讓局部變量變成全局變量。

  • 給未聲明的變量賦值
  • 使用指向全局對(duì)象的 this
  1. function createGlobalVariables() { 
  2.     leaking1 = '變成全局變量了'; // 給未聲明的變量賦值 
  3.     this.leaking2 = '這也是全局變量'; // 'this' 指向全局對(duì)象 
  4. }; 
  5. createGlobalVariables(); 
  6. window.leaking1; // '變成全局變量了' 
  7. window.leaking2; // '這也是全局變量' 

如何避免: 嚴(yán)格模式 ("use strict") 會(huì)避免意外的全局變量,以上代碼在嚴(yán)格模式下會(huì)報(bào)錯(cuò)。

2、閉包

函數(shù)作用域變量在函數(shù)執(zhí)行完后會(huì)被清理,前提是在函數(shù)外部沒有引用它。閉包會(huì)讓變量一直處于被引用狀態(tài),即使它的執(zhí)行上下文和作用域已經(jīng)不存在了。

  1. function outer() { 
  2.     const Array = []; 
  3.     return function inner() { 
  4.         bigArray.push('Hello');  
  5.         console.log('Hello'); 
  6.     }; 
  7. }; 
  8. const sayHello = outer(); // 包含了對(duì) inner 的引用 
  9.  
  10. function repeat(fn, num) { 
  11.     for (let i = 0; i < num; i++){ 
  12.         fn(); 
  13.     } 
  14. repeat(sayHello, 10); // 每次調(diào)用 sayHello 都會(huì)添加 'Hello' 到potentiallyHugeArray  
  15.  
  16. // 如果是10萬次呢?闊怕:repeat(sayHello, 100000) 

上面例子中的數(shù)組 bigArray 沒有從任何函數(shù)中直接返回,因此無法直接訪問,但是它卻不停地膨脹,取決于我們調(diào)用了多少次 function inner()。

如何避免: 閉包是 JavaScript 語言的特性之一,如果無法避開,那就請(qǐng)注意兩點(diǎn):

  • 清楚閉包是何時(shí)創(chuàng)建的,以及哪些對(duì)象會(huì)被保留在內(nèi)存中;
  • 清楚閉包的生命周期和用途(尤其是當(dāng)做回調(diào)函數(shù)的時(shí)候)

3、定時(shí)器

在 setTimeout 或 setInterval 的回調(diào)函數(shù)中引用某些對(duì)象,是防止被 GC 回收的常見做法。如果在代碼里設(shè)置循環(huán)定時(shí)器(setTimeout也能像setInterval一樣定時(shí)重復(fù)執(zhí)行,只要設(shè)置成遞歸調(diào)用),只要定時(shí)器還在運(yùn)行,回調(diào)函數(shù)中的對(duì)象就會(huì)一直保持在內(nèi)存中。

下面的例子中,data 對(duì)象會(huì)在清除定時(shí)器后被 GC 回收。但我們沒有獲取 setInterval的返回值,也就沒辦法用代碼清除這個(gè)定時(shí)器,因此盡管完全沒有用到,data.hugeString 也會(huì)一直保留在內(nèi)存中,直到進(jìn)程結(jié)束。

  1. function setCallback() { 
  2.     const data = { 
  3.         counter: 0, 
  4.         hugeString: new Array(100000).join('x'
  5.     }; 
  6.     return function cb() { 
  7.         data.counter++; // data 對(duì)象現(xiàn)在已經(jīng)屬于回調(diào)函數(shù)的作用域了 
  8.         console.log(data.counter); 
  9.     } 
  10. setInterval(setCallback(), 1000); // 沒法停止定時(shí)器了 

如何避免: 對(duì)于生命周期不確定的回調(diào)函數(shù),我們應(yīng)該:

  • 注意被定時(shí)器回調(diào)函數(shù)引用的對(duì)象
  • 使用定時(shí)器返回的句柄,在必要時(shí)清除它

也可以通過分離變量的方式,避免對(duì)大對(duì)象的引用:

  1. function setCallback() { 
  2.     // 分開定義變量 
  3.     let counter = 0; 
  4.     const hugeString = new Array(100000).join('x'); // setCallback執(zhí)行完即可被回收 
  5.     return function cb() { 
  6.         counter++; // 只剩 counter 位于回調(diào)函數(shù)作用域 
  7.         console.log(counter); 
  8.     } 
  9.  
  10. const timerId = setInterval(setCallback(), 1000); // 保存定時(shí)器 ID 
  11.  
  12. // 執(zhí)行某些操作 ... 
  13.  
  14. clearInterval(timerId); // 停止定時(shí)器 

4、事件監(jiān)聽器

活動(dòng)的事件監(jiān)聽器會(huì)阻止作用域內(nèi)的變量被 GC 回收。事件監(jiān)聽器一直處于活動(dòng)狀態(tài),直到用 removeEventListener() 顯式移除,或者關(guān)聯(lián)的 DOM 元素被移除。

對(duì)于有些事件來說,監(jiān)聽器需要一直保留,直到頁面被銷毀。比如按鈕點(diǎn)擊事件,我們可能需要重復(fù)使用。但是,有時(shí)候我們希望某個(gè)事件只執(zhí)行特定次數(shù)。

  1. const hugeString = new Array(100000).join('x'); 
  2. document.addEventListener('keyup'function() { // 匿名監(jiān)聽器無法移除 
  3.     doSomething(hugeString); // hugeString 會(huì)一直處于回調(diào)函數(shù)的作用域內(nèi) 
  4. }); 

上面例子中的事件監(jiān)聽器用了匿名函數(shù),這樣就沒法用removeEventListener()移除了。同時(shí),document元素也無法刪除,因此事件回調(diào)函數(shù)內(nèi)的變量會(huì)一直保留,哪怕我們只想觸發(fā)一次事件。

如何避免: 事件監(jiān)聽器不再需要時(shí),要記得解除綁定。使用具名函數(shù)方式獲取引用,通過removeEventListener()解除綁定。

  1. function listener() { 
  2.     doSomething(hugeString); 
  3. document.addEventListener('keyup', listener);  
  4. document.removeEventListener('keyup', listener);  

如果事件監(jiān)聽器只需要執(zhí)行一次, addEventListener()可以接受第三個(gè)參數(shù),是一個(gè)配置對(duì)象。指定{once: true},監(jiān)聽器函數(shù)會(huì)在事件觸發(fā)一次執(zhí)行后自動(dòng)移除(匿名函數(shù)也可以)。

  1. document.addEventListener('keyup'function listener(){ 
  2.     doSomething(hugeString); 
  3. }, {once: true}); // 執(zhí)行一次后自動(dòng)移除事件監(jiān)聽器 

5、緩存

如果持續(xù)不斷地往緩存里增加數(shù)據(jù),沒有定時(shí)清除無用的對(duì)象,也沒有限制緩存大小,那么緩存就會(huì)像滾雪球一樣越來越大。

  1. let user_1 = { name"Kayson", id: 12345 }; 
  2. let user_2 = { name"Jerry", id: 54321 }; 
  3. const mapCache = new Map(); 
  4.  
  5. function cache(obj){ 
  6.     if (!mapCache.has(obj)){ 
  7.         const value = `${obj.name} has an id of ${obj.id}`; 
  8.         mapCache.set(obj, value); 
  9.  
  10.         return [value, 'computed']; 
  11.     } 
  12.  
  13.     return [mapCache.get(obj), 'cached']; 
  14.  
  15. cache(user_1); // ['Kayson has an id of 12345''computed'
  16. cache(user_1); // ['Kayson has an id of 12345''cached'
  17. cache(user_2); // ['Jerry has an id of 54321''computed'
  18.  
  19. console.log(mapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321"
  20. user_1 = null;  
  21.  
  22. //Garbage Collector 
  23. console.log(mapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321") // 依然在緩存里 

上面的例子中,緩存依然保留了user_1 的數(shù)據(jù)。因此我們需要把不再使用的數(shù)據(jù)從緩存中刪除。

可能的解決方案: 為了解決這個(gè)問題,可以使用 WeakMap。 WeakMap 是一種數(shù)據(jù)結(jié)構(gòu),它只用對(duì)象作為鍵,并保持對(duì)象鍵的弱引用,如果這個(gè)對(duì)象被置空了,相關(guān)的鍵值對(duì)會(huì)被 GC 自動(dòng)回收。

  1. let user_1 = { name"Kayson", id: 12345 }; 
  2. let user_2 = { name"Jerry", id: 54321 }; 
  3. const weakMapCache = new WeakMap(); 
  4.  
  5. function cache(obj){ 
  6.     // 代碼跟前一個(gè)例子相同,只不過用的是 weakMapCache 
  7.  
  8.     return [weakMapCache.get(obj), 'cached']; 
  9.  
  10. cache(user_1); // ['Kayson has an id of 12345''computed'
  11. cache(user_2); // ['Jerry has an id of 54321''computed'
  12. console.log(weakMapCache); // ((…) => "Kayson has an id of 12345", (…) => "Jerry has an id of 54321"
  13. user_1 = null;  
  14.  
  15. // Garbage Collector 
  16.  
  17. console.log(weakMapCache); // ((…) => "Jerry has an id of 54321") - 第一條記錄已被 GC 刪除 

6、分離的 DOM 元素

如果 DOM 節(jié)點(diǎn)被 JavaScript 代碼直接引用,即使從 DOM 樹分離,也不會(huì)被 GC 回收。

下面的例子中,removeChild() 達(dá)不到預(yù)期效果,堆快照會(huì)顯示HTMLDivElement處于分離狀態(tài),因?yàn)橛袀€(gè)變量指向了這個(gè)div。

  1. function createElement() { 
  2.     const div = document.createElement('div'); 
  3.     div.id = 'detached'
  4.     return div; 
  5.  
  6. // 即使調(diào)用了deleteElement() ,依然保存著 DOM 元素的引用 
  7. const detachedDiv = createElement(); 
  8. document.body.appendChild(detachedDiv); 
  9. function deleteElement() { 
  10. document.body.removeChild(document.getElementById('detached')); 
  11.  
  12. deleteElement(); // 堆快照顯示: detached div#detached 

如何避免: 一種方法是把DOM 引用限制為局部作用域。

  1. function createElement() {...} //  
  2. // DOM 引用位于函數(shù)作用域內(nèi) 
  3.  
  4. function appendElement() { 
  5.     const detachedDiv = createElement(); 
  6.     document.body.appendChild(detachedDiv); 
  7.  
  8. appendElement(); 
  9.  
  10. function deleteElement() { 
  11.      document.body.removeChild(document.getElementById('detached')); 
  12.  
  13. deleteElement(); 

總結(jié)

對(duì)于重要的前端應(yīng)用,定位和解決 JavaScript 內(nèi)存問題是一項(xiàng)頗具挑戰(zhàn)性的任務(wù)。因此,理解典型的內(nèi)存泄露原因,從而在源頭上避免,是做好內(nèi)存管理的必要工作。希望本文總結(jié)的造成內(nèi)存泄漏的六大來源對(duì)你有所啟發(fā),在寫代碼的時(shí)候有所防范。

 

責(zé)任編輯:華軒 來源: 大道至簡
相關(guān)推薦

2021-08-05 15:28:22

JS內(nèi)存泄漏

2009-06-10 22:03:40

JavaScript內(nèi)IE內(nèi)存泄漏

2010-07-16 09:11:40

JavaScript內(nèi)存泄漏

2022-05-26 09:51:50

JavaScrip內(nèi)存泄漏

2013-01-22 11:31:00

2022-09-28 10:35:31

JavaScript代碼內(nèi)存泄漏

2023-02-20 15:27:30

開發(fā)JavaScript內(nèi)存管理

2021-02-26 00:49:00

DMARC郵件安全信息泄漏

2012-04-06 10:04:21

2024-03-11 08:22:40

Java內(nèi)存泄漏

2023-12-18 10:45:23

內(nèi)存泄漏計(jì)算機(jī)服務(wù)器

2012-02-22 21:28:58

內(nèi)存泄漏

2014-07-21 14:40:43

Android內(nèi)存

2014-07-28 15:01:56

Android內(nèi)存

2022-08-31 12:15:09

JavaScript代碼優(yōu)化

2024-01-15 16:28:42

ChatGPTGenAI人工智能

2016-05-17 09:42:16

2019-06-24 19:00:09

JavaScript內(nèi)存泄漏垃圾回收

2014-07-31 10:48:09

Android內(nèi)存管理OOM

2015-03-30 11:18:50

內(nèi)存管理Android
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)