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

如何避免JavaScript內(nèi)存泄漏?

開(kāi)發(fā) 開(kāi)發(fā)工具
這篇文章通過(guò)一些簡(jiǎn)單的例子介紹內(nèi)存泄漏的調(diào)查方法、總結(jié)內(nèi)存泄漏出現(xiàn)的原因和常見(jiàn)情況,并針對(duì)每種情況總結(jié)如何避免內(nèi)存泄漏。希望能對(duì)大家有所幫助。

 [[415664]]

很多開(kāi)發(fā)者可能平時(shí)并不關(guān)心自己維護(hù)的頁(yè)面是否存在內(nèi)存泄漏,原因可能是剛開(kāi)始簡(jiǎn)單的頁(yè)面內(nèi)存泄漏的速度很緩慢,在造成嚴(yán)重卡頓之前可能就被用戶(hù)刷新了,問(wèn)題也就被隱藏了,但是隨著頁(yè)面越來(lái)越復(fù)雜,尤其當(dāng)你的頁(yè)面是 SAP 方式交互時(shí),內(nèi)存泄漏的隱患便越來(lái)越嚴(yán)重,直到突然有一天用戶(hù)反饋說(shuō):“操作一會(huì)兒頁(yè)面就卡住不動(dòng)了,也不知道為什么,以前不這樣的呀”。

這篇文章通過(guò)一些簡(jiǎn)單的例子介紹內(nèi)存泄漏的調(diào)查方法、總結(jié)內(nèi)存泄漏出現(xiàn)的原因和常見(jiàn)情況,并針對(duì)每種情況總結(jié)如何避免內(nèi)存泄漏。希望能對(duì)大家有所幫助。

一、一個(gè)簡(jiǎn)單的例子

先看一個(gè)簡(jiǎn)單的例子,下面是這個(gè)例子對(duì)應(yīng)的代碼:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3.   <head> 
  4.     <meta charset="UTF-8" /> 
  5.     <title>memory-leak</title> 
  6.   </head> 
  7.   <body> 
  8.     <p>push date for <button class="count-date">0</button> times</p> 
  9.  
  10.     <p>add Date: <button class="push-date">add date</button></p> 
  11.  
  12.     <p>clear: <button class="clear">clear</button></p> 
  13.  
  14.     <script> 
  15.       const pushDate = document.querySelector(".push-date"); 
  16.       const dateCount = document.querySelector(".count-date"); 
  17.  
  18.       let dateAry = []; 
  19.  
  20.       let dateNum = 0; 
  21.  
  22.       // 【寫(xiě)入 date】 
  23.       pushDate.addEventListener("click", () => { 
  24.         dateCount.innerHTML = `${++dateNum}`; 
  25.  
  26.         for (let j = 0; j < 3000; ++j) { 
  27.           dateAry.push(new Date()); 
  28.         } 
  29.       }); 
  30.  
  31.       const clear = document.querySelector(".clear"); 
  32.  
  33.       // 【回收內(nèi)存】 
  34.       clear.addEventListener("click", () => { 
  35.         dateAry = []; 
  36.         dateCount.innerHTML = "0"
  37.       }); 
  38. </script> 
  39.   </body> 
  40. </html> 

 代碼 1

代碼 1 的邏輯很簡(jiǎn)單:點(diǎn)擊“add date”按鈕時(shí)會(huì)向 dateAry 數(shù)組中 push 3000 個(gè) new Date 對(duì)象,點(diǎn)擊“clear”按鈕時(shí)將 dateAry 清空。很明顯,“add date”操作會(huì)造成內(nèi)存占用不斷增長(zhǎng),如果將這個(gè)邏輯用在實(shí)際應(yīng)用中便會(huì)造成內(nèi)存泄漏(不考慮故意將代碼邏輯設(shè)計(jì)成這樣的情況),下面我們看一下如何調(diào)查這種內(nèi)存增長(zhǎng)出現(xiàn)的原因以及如何找出內(nèi)存泄漏點(diǎn)。

1.heap snapshot

為了避免瀏覽器插件的干擾,我們?cè)?chrome 中新建一個(gè)無(wú)痕窗口打開(kāi)上述代碼。然后在 chrome 的 devtools 中的 Memory 工具中找到 “Heap Snapshot”工具,點(diǎn)擊左上角的錄制按鈕錄制一個(gè) Snapshot,然后點(diǎn)擊“add date”按鈕,在手動(dòng)觸發(fā) GC(Garbage Collect)之后,再次錄制一個(gè) Snapshot,反復(fù)執(zhí)行上述操作若干次,像圖 1 中操作的那樣,得到一系列的 Snapshot。

圖 1 錄制 Snapshot

圖 2 是我們剛剛得到的 Snapshot 組,其中的第一個(gè)是頁(yè)面初始加載的時(shí)候錄制的,不難發(fā)現(xiàn),從第二個(gè)開(kāi)始,每個(gè) Snapshot 相比于上一個(gè)其大小都增加了約 200KB,我們點(diǎn)擊選擇 Snapshot 2,在 class filter 輸入框中處輸入 date,可以得到 Snapshot 2 中所有被 Date 構(gòu)造器構(gòu)造出來(lái)的 JS 對(duì)象,也就是 Date 對(duì)象,這里看到的構(gòu)造器跟瀏覽器內(nèi)部的實(shí)現(xiàn)有關(guān),不必跟 JS 的對(duì)象對(duì)應(yīng)。

選中一個(gè) Date 對(duì)象,在下面的面板中可以看到所選對(duì)象的持有鏈以及相關(guān)持有對(duì)象的內(nèi)存的保留大小(Retained Size),從圖中可以看出選中的 Date 對(duì)象是 Array 的第 1 個(gè)元素(index 從 0 開(kāi)始),而這個(gè) Array 的持有者是 system/Context 上下文中的 dateAry,system/Context 上下文就是代碼中 script 標(biāo)簽的上下文,我們可以看到在這個(gè) dataAry 的保留大小是 197KB,我們?cè)偾械?Snapshot 3,用相同的方式查看內(nèi)存持有和大小,可以發(fā)現(xiàn) Snapshot 3 中的 dataAry 的保留大小變成了 386KB,相比于 Snapshot 2 增漲了約 200KB!逐一比較后面的 Snapshot 4、5 后也能得到相同的對(duì)比結(jié)果,即下一個(gè) Snapshot 中的 dateAry 比上一個(gè)的保留大小大約 200KB。

圖 2 錄制的 Snapshot 組

參考【代碼 1】我們可以知道,“add date”按鈕在被點(diǎn)擊時(shí),會(huì)向 dateAry 數(shù)組中 push 3000 個(gè)新的 Date 對(duì)象,而在圖 2 中的 Date 構(gòu)造器的右側(cè)可以看到這 3000 個(gè) Date 對(duì)象(Date x 3000),它對(duì)應(yīng)的正式我們的循環(huán)創(chuàng)建的那 3000 個(gè) Date 對(duì)象。綜合上面的操作我們可以知道,chorome devtools 中的 Memroy 的 Heap Snapshot 工具可以錄制某一個(gè)時(shí)刻的所有內(nèi)存對(duì)象,也就是一個(gè)“快照”,快照中按“構(gòu)造器”分組,展示了所有被記錄下來(lái)的 JS 對(duì)象。

如果這個(gè)頁(yè)面是一個(gè)實(shí)際服務(wù)于用戶(hù)的網(wǎng)站的某個(gè)頁(yè)面話(huà)(用戶(hù)可能非常頻繁的點(diǎn)擊“add date”按鈕,作者可能想記錄用戶(hù)點(diǎn)擊的次數(shù)?也許吧,雖然我也不知道他什么要這么做)隨著用戶(hù)使用時(shí)間的增長(zhǎng),“add date”按鈕的反應(yīng)就會(huì)越來(lái)越慢,整體頁(yè)面也隨之越來(lái)越卡,原因除了系統(tǒng)的內(nèi)存資源被占用之外,還有 GC 的頻率和時(shí)長(zhǎng)增長(zhǎng),如圖 3 所示,因?yàn)?GC 執(zhí)行的過(guò)程中 JS 的執(zhí)行是被暫停的,所以頁(yè)面就會(huì)呈現(xiàn)出越來(lái)越卡的樣子。

圖 3 Performance 錄制的 GC 占比

圖 4 chrome 的任務(wù)管理器

最終:

圖 5 內(nèi)存占用過(guò)高導(dǎo)致瀏覽器崩潰

那么,在這個(gè)“實(shí)際”的場(chǎng)景下,如何找出那“作祟”的 3000 個(gè) Date 對(duì)象呢?我們首先想到的應(yīng)該是就是:之前不是錄制了好多個(gè) Snapshot 嗎?可不可以把它們做對(duì)比找到“差異”呢,從差異中找到增長(zhǎng)的地方不就行了?思路非常正確,在此之前我們?cè)俜治鲆幌逻@幾個(gè) Snapshot:每次點(diǎn)擊“add date”按鈕、手動(dòng)觸發(fā) GC、得到的 Snapshot 的大小相比上一次都有所增加,如果這種內(nèi)存的增長(zhǎng)現(xiàn)象不符合“預(yù)期”的話(huà)(顯然在這個(gè)“實(shí)際”的例子中是不符合預(yù)期的),那么這里就有很大的嫌疑存在內(nèi)存泄漏。

這個(gè)時(shí)候我們選中 Snapshot 2,在圖 2 所示的 " Summary" 處選擇“Comparison”,在右側(cè)的 "All objects" 處選擇 Snapshot 1,這樣一來(lái),Constructor 里展示便是 Snapshot 1 和 Snapshot 2 的對(duì)比,通過(guò)觀察不難發(fā)現(xiàn),圖中的 +144KB 最值得懷疑,于是我們選中它的構(gòu)造器 Date,展開(kāi)選中任意子項(xiàng)看詳情,發(fā)現(xiàn)其是被 Array 構(gòu)造器構(gòu)造出來(lái)的 dateAry 持有的(即 dateAry 中的一員),并且 dateAry 被三個(gè)地方持有,其中系統(tǒng)內(nèi)部的 array 我們不用理會(huì),圖 6 中寫(xiě)有 "context in ()" 地方給了我們持有 dateAry 的 context 所在的位置,點(diǎn)擊便可以跳到代碼所在的位置了,整個(gè)操作如圖 6 所示:

圖 6 定位代碼位置

這里有一個(gè)值得注意的地方,圖 6 中的 “context in () @449305” 中的 "()",這里之所以展示為了 "()" 是因?yàn)榇a中用了“匿名函數(shù)”(代碼 2 中第 2 行的箭頭函數(shù)):

  1. // 【寫(xiě)入 date】 
  2. pushDate.addEventListener("click", () => { 
  3.     dateCount.innerHTML = `${++dateNum}`; 
  4.  
  5.     for (let j = 0; j < 3000; ++j) { 
  6.         dateAry.push(new Date()); 
  7.     } 
  8. }); 

代碼 2 匿名函數(shù)

但是如果我們給函數(shù)起一個(gè)名字,如下面的代碼所示,也就是如果我們使用具名函數(shù)(代碼3 第 2 行函數(shù) add)或者將函數(shù)賦值給一個(gè)變量并使用這個(gè)變量(第 10 和 18 行的行為)的時(shí)候,devtools 中都可以看到相應(yīng)的函數(shù)的名字,這也就可以幫助我們更好的定位代碼,如圖 7 所示。

  1. // 【寫(xiě)入 date】 
  2. pushDate.addEventListener("click"function add() { 
  3.     dateCount.innerHTML = `${++dateNum}`; 
  4.  
  5.     for (let j = 0; j < 3000; ++j) { 
  6.         dateAry.push(new Date()); 
  7.     } 
  8. }); 
  9.  
  10. const clear = document.querySelector(".clear"); 
  11.  
  12. const doClear = function () { 
  13.     dateAry = []; 
  14.     dateCount.innerHTML = "0"
  15. }; 
  16.  
  17. // 【回收內(nèi)存】 
  18. clear.addEventListener("click", doClear); 

代碼 3 具名函數(shù)

圖 7 具名函數(shù)方便定位

這樣我們便找到了代碼可疑的地方,只需要將代碼的作者抓過(guò)來(lái)對(duì)著他一頓“分析”這個(gè)內(nèi)存泄漏的問(wèn)題基本就水落石出了。

其實(shí),Snapshot 除了“Comparison”之外還有一個(gè)更便捷的用于對(duì)比的入口,在這里直接可以看到在錄制 Snapshot 1 和 Snapshot 2 兩個(gè)時(shí)間點(diǎn)之間被分配出來(lái)的內(nèi)存,用這種方式也可以定位到那個(gè)可疑的 Date x 3000:

圖 8 Snapshot 比較器

上文件介紹的是用 Heap Snapshot 尋找內(nèi)存泄漏點(diǎn)的方法,這個(gè)方法的優(yōu)點(diǎn):可以錄制多個(gè) Snapshot,然后方便的兩兩比較,并且能看到 Snapshot 中的全量?jī)?nèi)存,這一點(diǎn)是下文要講的“Allocation instrumentation on timeline”方法不具備的,并且這種方法可以更加方便地查找后面會(huì)講的因 Detached Dom 導(dǎo)致的內(nèi)存泄漏。

2.Allocation instrumentation on timeline

但是,不知道你有沒(méi)有覺(jué)得,這種高頻率地錄制 Snapshot、對(duì)比、再對(duì)比的方式有點(diǎn)兒麻煩?我需要不斷的去點(diǎn)擊“add date”,然后鼠標(biāo)又要跑過(guò)去點(diǎn)擊手動(dòng) GC、錄制 Snapshot、等待錄制完畢,再去操作,再去錄制。有沒(méi)有簡(jiǎn)單一些的方式來(lái)查找內(nèi)存泄漏?這個(gè)時(shí)候我們回到 Memory 最初始的界面,你突然發(fā)現(xiàn) “Heap snapshot”下面還有一個(gè) radio:“Allocation instrumentation on timeline”,并且這個(gè) radio 下面的介紹文案的最后寫(xiě)著:“Use this profile type to isolate memory leaks”,原來(lái)這是一個(gè)專(zhuān)門(mén)用于調(diào)查內(nèi)存泄漏的工具!于是,我們選中這個(gè) radio,點(diǎn)擊開(kāi)始錄制按鈕,然后將注意力放在頁(yè)面上,然后你發(fā)現(xiàn)當(dāng)點(diǎn)擊“add date”按鈕時(shí),右面錄制的 timeline 便會(huì)多出一個(gè)心跳:

圖 9 Allocation instrumentation on timeline

如圖 9 所示,每當(dāng)我們點(diǎn)擊“add date”按鈕時(shí),右面都有一個(gè)對(duì)應(yīng)的心跳,當(dāng)我們點(diǎn)擊“clear”按鈕時(shí),剛才出現(xiàn)的所有心跳便全都“縮回”去了,于是我們得出結(jié)論:每一個(gè)“心跳”都是一次內(nèi)存分配,其高度代表內(nèi)存分配的量,在之后的時(shí)間推移過(guò)程中,如果剛才心跳對(duì)應(yīng)的被分配的內(nèi)存被 GC 回收了,“心跳”便會(huì)跟著變化為回收之后的高度。于是,我們便擺脫了在 Snapshot 中來(lái)回操作、錄制的窘境,只需要將注意力集中在頁(yè)面的操作上,并觀察哪個(gè)操作在右邊的時(shí)間線(xiàn)變化中是可疑的。

經(jīng)過(guò)一系列操作,我們發(fā)現(xiàn)“add date”這個(gè)按鈕的點(diǎn)擊行為很可疑,因?yàn)樗峙涞膬?nèi)存不會(huì)自動(dòng)被回收,也就是只要點(diǎn)擊一次,內(nèi)存就會(huì)增長(zhǎng)一點(diǎn),我們停止錄制,得到了一個(gè) timeline 的 Snapshot,這個(gè)時(shí)候如果我們點(diǎn)擊某個(gè)心跳的話(huà):

圖 10 點(diǎn)擊某個(gè)心跳

熟悉的 Date x 3000 又出現(xiàn)了(圖 11),點(diǎn)擊一個(gè) Date 對(duì)象看持有鏈,接下來(lái)便跟上文 Snapshot 的持有鏈分析一樣了:

圖 11 通過(guò) timeline 找到泄漏點(diǎn)

這個(gè)方法的優(yōu)點(diǎn)上文已經(jīng)說(shuō)明,可以非常直觀、方便的觀察內(nèi)存隨可疑操作的分配與回收過(guò)程,可以方便的觀察每次分配的內(nèi)存。它的缺點(diǎn):錄制時(shí)間較長(zhǎng)時(shí) devtools 收集錄制結(jié)果的時(shí)間會(huì)很長(zhǎng),甚至有時(shí)候會(huì)卡死瀏覽器;下文會(huì)講到 detached DOM,這個(gè)工具不能比較出 detached DOM,而 heap snapshot 可以。

3.performance

devtools 中的 Performance 面版中也有一個(gè) Memory 功能,下面看一下它如何使用。我們把 Memory 勾選上,并錄制一個(gè) performance 結(jié)果:

圖 12 Performance 的錄制過(guò)程

在圖 12 中可以看到,在錄制的過(guò)程中我們連續(xù)點(diǎn)擊“add date”按鈕 10 次,然后點(diǎn)擊一次“clear”按鈕,然后再次點(diǎn)擊“add date” 10 次,得到的最終結(jié)果如圖 13 所示:

圖 13 Performance 的錄制結(jié)果

在圖 13 中我們可以得到下面的信息:

  • 整個(gè)操作過(guò)程中內(nèi)存的走勢(shì):參見(jiàn)圖 13 下方的位置,第一輪點(diǎn)擊 10 次的過(guò)程中內(nèi)存不斷增長(zhǎng),點(diǎn) clear 之后內(nèi)存斷崖式下跌,第二輪點(diǎn)擊 10 次內(nèi)存又不斷增長(zhǎng)。這也是這個(gè)工具的主要作用:得到可疑操作的內(nèi)存走勢(shì)圖,如果內(nèi)存持續(xù)走高則有理由懷疑此操作由內(nèi)存泄漏的可能。
  • 內(nèi)存的增長(zhǎng)量:參見(jiàn) JS Heap 位置,鼠標(biāo)放上去可以看見(jiàn)每個(gè)階梯上下位置的內(nèi)存增長(zhǎng)/下跌的量
  • 通過(guò)在 timeline 中定位某個(gè)“階梯”,我們也能找到可疑的代碼,如圖 14 所示:

圖 14 通過(guò) Performance 定位問(wèn)題代碼

這種方法的優(yōu)點(diǎn):可以直觀得看到內(nèi)存的總體走勢(shì),并且同時(shí)得到所有操作過(guò)程中的函數(shù)調(diào)用棧和時(shí)間等信息。缺點(diǎn):沒(méi)有具體的內(nèi)存分配的細(xì)節(jié),錄制的過(guò)程不能實(shí)時(shí)看到內(nèi)存分配的過(guò)程。

二、內(nèi)存泄漏出現(xiàn)的場(chǎng)景

1.全局

JS 采用標(biāo)記清掃法去回收無(wú)法訪問(wèn)的內(nèi)存對(duì)象,被掛載在全局對(duì)象(在瀏覽器中即指的是 window 對(duì)象,在垃圾回收的角度上稱(chēng)其為根節(jié)點(diǎn),也叫 GC root)上的屬性所占用內(nèi)存是不會(huì)被回收的,因?yàn)槠涫鞘冀K可以訪問(wèn)的,這也符合“全局”的命名含義。

解決方案就是避免用全局對(duì)象存儲(chǔ)大量的數(shù)據(jù)。

2.閉包(closure)

我們把【代碼 1】稍加改動(dòng)便可以得到一個(gè)閉包導(dǎo)致內(nèi)存泄漏的版本:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3.   <head> 
  4.     <meta charset="UTF-8" /> 
  5.     <title>memory-leak</title> 
  6.   </head> 
  7.   <body> 
  8.     <p>push date for <button class="count-date">0</button> times</p> 
  9.  
  10.     <p>add Date: <button class="push-date">add date</button></p> 
  11.  
  12.     <p>clear: <button class="clear">clear</button></p> 
  13.  
  14.     <script> 
  15.       const pushDate = document.querySelector(".push-date"); 
  16.       const dateCount = document.querySelector(".count-date"); 
  17.  
  18.       let ary = []; 
  19.  
  20.       const wrap = () => { 
  21.         const dateAry = Array(3_000).map(() => new Date()); 
  22.  
  23.         const inner = () => { 
  24.           return dateAry; 
  25.         }; 
  26.  
  27.         return inner
  28.       }; 
  29.  
  30.       // 【寫(xiě)入 date】 
  31.       pushDate.addEventListener("click"function add() { 
  32.         ary.push(wrap()); 
  33.         dateCount.innerHTML = `${ary.length}`; 
  34.       }); 
  35.  
  36.       const clear = document.querySelector(".clear"); 
  37.  
  38.       // 【回收內(nèi)存】 
  39.       clear.addEventListener("click"function clear() { 
  40.         ary = []; 
  41.         dateCount.innerHTML = `${ary.length}`; 
  42.       }); 
  43. </script> 
  44.   </body> 
  45. </html> 

代碼 3 閉包導(dǎo)致內(nèi)存泄漏

將上述代碼加載到 chrome 中,并用 timeline 的方式錄制一個(gè) Snapshot,得到的結(jié)果如圖 15 所示:

圖 15 閉包的錄制結(jié)果

我們選中 index = 2 的心跳,可以看到 Constructor 里面出現(xiàn)了一個(gè) "(closure)",我們展開(kāi)這個(gè) closure,可以看到里面的 "inner()",inner() 后面的 "()" 表示 inner 是一個(gè)函數(shù),這時(shí)候你可能會(huì)問(wèn):“圖中的 Constructor 的 Retained Size 大小都差不多,為什么你要選 (closure)?”,正是因?yàn)闆](méi)有明顯占比較高的 Retained Size 我們才隨便選一個(gè)調(diào)查,后面你會(huì)發(fā)現(xiàn)不管你選了哪一個(gè)最后的調(diào)查鏈路都是殊途同歸的。

我們?cè)谙旅娴?Retainers 中看下 inner() 的持有細(xì)節(jié):從下面的 Retainers 中可以看出 inner() 這個(gè) closure 是某個(gè) Array 的第 2 項(xiàng)(index 從 0 開(kāi)始),而這個(gè)數(shù)組的持有者是 system/Context(即全局) 中的 ary,通過(guò)觀察可以看到 ary 的持有大小(Retained Size)是 961KB 大約等于 192KB 的 5 倍,5 即是我們點(diǎn)擊“add date”按鈕的次數(shù),而下面的 5 個(gè) "previous in system/Context" 每個(gè)大小都是 192KB,而它們最終都是被某個(gè) inner() 閉包持有,至此我們便可以得出結(jié)論:全局中有一個(gè) ary 數(shù)組,它的主要內(nèi)存是被 inner() 填充的,通過(guò)藍(lán)色的 index.html:xx 處的代碼入口定位到代碼所在地看一下一切就都了然了,原來(lái)是 inner() 閉包內(nèi)部持有了一個(gè)大對(duì)象,并且所有的 inner() 閉包及其持有的大對(duì)象都被 ary 對(duì)象持有,而 ary 對(duì)象是全局的不會(huì)被回收,導(dǎo)致了內(nèi)存泄漏(如果這種行為不符合預(yù)期的話(huà))。返回去,如果這個(gè)時(shí)候你選擇上面提到的 system/Context 構(gòu)造器,你會(huì)看到(見(jiàn)圖 16,熟悉吧):

圖 16 system/Context

也就是你選擇的 system/Context 其實(shí)是 inner() 閉包的上下文對(duì)象(context),而此上下文持有了 192KB 內(nèi)存,通過(guò)藍(lán)色的 index.html:xx 又可以定位到問(wèn)題代碼了。如果你像圖 17 一樣選擇了 Date 構(gòu)造器進(jìn)行查看的話(huà)也可以最終定位到問(wèn)題,此處將分析過(guò)程留給讀者自己進(jìn)行:

圖 17 選中 Date 構(gòu)造器

3.Detached DOM

我們先看一下下面的代碼,并用 chrome 載入它:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3.   <head> 
  4.     <meta charset="UTF-8" /> 
  5.     <title>memory-leak</title> 
  6.   </head> 
  7.   <body> 
  8.     <p>add Date: <button class="push-date">add date</button></p> 
  9.  
  10.     <p>delete button: <button class="del">del</button></p> 
  11.  
  12.     <script> 
  13.       const addDate = document.querySelector(".push-date"); 
  14.       const del = document.querySelector(".del"); 
  15.  
  16.       function add() {} 
  17.  
  18.       addDate.addEventListener("click"add); 
  19.  
  20.       del.addEventListener("click"function del() { 
  21.         addDate.remove(); 
  22.       }); 
  23. </script> 
  24.   </body> 
  25. </html> 

代碼 4 Detached Dom

然后我們采用 Heap Snapshot 的方式將點(diǎn)擊“del”按鈕前后的兩個(gè) snapshot 錄制下來(lái),得到的結(jié)果如圖 6 所示。我們選用和 snapshot 1 對(duì)比的方式并在 snapshot 2 的過(guò)濾器中輸入 "detached"。我們觀察得到的篩選結(jié)果的 "Delta" 列,其中不為 0 的列如下:

Constructor # Delta
Detached HTMLButtonElement +1
Detached EventListener +1
Detached InternalNode +2
Detached Text +1
Detached V8EventListener +1

要解釋上述表格需要先介紹一個(gè)知識(shí)點(diǎn):DOM 對(duì)象被回收需要同時(shí)滿(mǎn)足兩個(gè)條件,1、DOM 在 DOM 樹(shù)中被刪掉;2、DOM 沒(méi)有被 JS 對(duì)象引用。其中第二點(diǎn)還是比較容易被忽視的。正如上面的例子所示,Detached HTMLButtonElement +1 代表有一個(gè) button DOM 被從組件樹(shù)中刪掉了,但是仍有 JS 引用之(我們不考慮有意為之的情況)。

相似的,Detached EventListener 也是因?yàn)?DOM 被刪掉了,但是事件沒(méi)有解綁,于是 Detached 了,解決方案也很簡(jiǎn)單:及時(shí)解綁事件即可。

于是解決的方法就很簡(jiǎn)單了:參見(jiàn)代碼 5,回掉函數(shù) del 在執(zhí)行完畢時(shí)臨時(shí)變量會(huì)被回收,于是兩個(gè)條件就都同時(shí)滿(mǎn)足了,DOM 對(duì)象就會(huì)被回收掉,事件解綁了,Detached EventListener 也就沒(méi)有了。值得注意的是 table 元素,如果一個(gè) td 元素發(fā)生了 detached,則由于其自身引用了自己所在的 table,于是整個(gè) table 就也不會(huì)被回收了。

  1. <script> 
  2.       const del = document.querySelector(".del"); 
  3.  
  4.       function add() {} 
  5.  
  6.       document.querySelector(".push-date").addEventListener("click"add); 
  7.  
  8.       del.addEventListener("click"function del() { 
  9.         document.querySelector(".push-date").removeEventListener("click"add); 
  10.         document.querySelector(".push-date").remove(); 
  11.       }); 
  12. </script> 

代碼 5 Detached DOM 的解決方法

圖 18 Detached DOM 的 Snapshot

Performance monitor 工具

DOM/event listener 泄漏在編寫(xiě)輪播圖、彈窗、toast 提示這種工具的時(shí)候還是很容易出現(xiàn)的,chrome 的 devtools 中有一個(gè) Performance monitor 工具可以用來(lái)幫助我們調(diào)查內(nèi)存中是否有 DOM/event listener 泄漏。首先看一下代碼 6:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3.   <head> 
  4.     <meta charset="UTF-8" /> 
  5.     <title>memory-leak</title> 
  6.   </head> 
  7.   <body> 
  8.     <p>add Date: <button class="push-date">add date</button></p> 
  9.  
  10.     <div class="btn-list"></div> 
  11.  
  12.     <script> 
  13.       const btnList = document.querySelector(".btn-list"); 
  14.       const addDate = document.querySelector(".push-date"); 
  15.  
  16.       addDate.addEventListener("click"function del() { 
  17.         const btn = document.createElement("button"); 
  18.         btn.innerHTML = "a btn"
  19.         btnList.appendChild(btn); 
  20.       }); 
  21. </script> 
  22.   </body> 
  23. </html> 

代碼 6 不斷增加 DOM NODE

按照我們圖 19 的方式打開(kāi) Performance monitor 面版:

圖 19 打開(kāi) Performance monitor 工具

DOM Nodes 右側(cè)的數(shù)量是當(dāng)前內(nèi)存中的所有 DOM 節(jié)點(diǎn)的數(shù)量,包括當(dāng)前 document 中存在的和 detached 的以及計(jì)算過(guò)程中臨時(shí)創(chuàng)建的,每當(dāng)我們點(diǎn)擊一次“add date”按鈕,并手動(dòng)觸發(fā) GC 之后 DOM Nodes 的數(shù)量就 + 2,這是因?yàn)槲覀兿? document 中增加了一個(gè) button 節(jié)點(diǎn)和一個(gè) button 的文字節(jié)點(diǎn),就像圖 20 中所示。如果你寫(xiě)的 toast 組件在臨時(shí)插入到 document 并過(guò)一會(huì)兒執(zhí)行了 remove 之后處于了 detached 狀態(tài)的話(huà),Performance monitor 面版中的 DOM Nodes 數(shù)量就會(huì)不斷增加,結(jié)合 snapshot 工具你便可以定位到問(wèn)題所在了。值得一提的是,有的第三方的庫(kù)的 toast 便存在這個(gè)問(wèn)題,不知道你被坑過(guò)沒(méi)有。

圖 20 不斷增加的 DOM Nodes

4.console

這一點(diǎn)可能有人不會(huì)留意到,控制臺(tái)打印的內(nèi)容是需要始終保持引用的存在的,這一點(diǎn)也是值得注意的,因?yàn)榇蛴∵^(guò)多過(guò)大對(duì)象的話(huà)也是會(huì)造成內(nèi)存泄漏的,如圖 21 所示(配合代碼 7)。解決方法便是不要肆意打印對(duì)象到控制臺(tái)中,只打印必要的信息出來(lái)。

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3.   <head> 
  4.     <meta charset="UTF-8" /> 
  5.     <title>memory-leak</title> 
  6.   </head> 
  7.   <body> 
  8.     <p>add Date: <button class="push-date">add date</button></p> 
  9.  
  10.     <div class="btn-list"></div> 
  11.  
  12.     <script> 
  13.       const addDate = document.querySelector(".push-date"); 
  14.  
  15.       addDate.addEventListener("click"function del() { 
  16.         const tmp = Array(3_000) 
  17.           .fill() 
  18.           .map(() => new Date()); 
  19.  
  20.         console.info(tmp); 
  21.       }); 
  22. </script> 
  23.   </body> 
  24. </html> 

代碼 7 console 導(dǎo)致內(nèi)存泄漏

圖 21 console 導(dǎo)致的內(nèi)存泄漏

三、總結(jié)

本文用了幾個(gè)簡(jiǎn)單的小例子介紹了內(nèi)存泄漏出現(xiàn)的時(shí)機(jī)、尋找泄漏點(diǎn)的方法并將各種方法的優(yōu)缺點(diǎn)進(jìn)行了對(duì)比,總結(jié)了避免出現(xiàn)內(nèi)存泄漏的注意點(diǎn)。希望能對(duì)讀者有所幫助。文中如果有本人理解錯(cuò)誤或書(shū)寫(xiě)錯(cuò)誤的地方歡迎留言指正。

參考

https://commandlinefanatic.com/cgi-bin/showarticle.cgi?article=art038

https://developer.chrome.com/docs/devtools/memory-problems/

https://www.bitdegree.org/learn/chrome-memory-tab

【本文為51CTO專(zhuān)欄作者“阿里巴巴官方技術(shù)”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】

戳這里,看該作者更多好文

 

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專(zhuān)欄
相關(guān)推薦

2022-05-26 09:51:50

JavaScrip內(nèi)存泄漏

2021-08-09 09:54:37

內(nèi)存泄漏JS 阿里云

2023-02-20 15:27:30

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

2024-01-30 10:12:00

Java內(nèi)存泄漏

2023-10-30 08:18:21

內(nèi)存泄漏Java

2020-06-08 09:18:59

JavaScript開(kāi)發(fā)技術(shù)

2024-12-19 14:42:15

C++內(nèi)存泄漏內(nèi)存管理

2024-02-01 09:58:40

Java內(nèi)存泄漏

2020-01-14 10:57:39

內(nèi)存泄漏虛擬機(jī)

2009-06-10 22:03:40

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

2010-07-16 09:11:40

JavaScript內(nèi)存泄漏

2023-12-18 10:45:23

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

2016-05-25 10:03:51

JavaScript內(nèi)存泄露

2021-04-22 07:41:46

JavaScript類(lèi)型轉(zhuǎn)換

2019-06-24 19:00:09

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

2010-08-27 13:19:46

2013-12-17 15:46:04

iOS開(kāi)發(fā)iOS 內(nèi)存泄漏

2011-08-15 10:16:55

內(nèi)存泄露

2017-01-05 19:34:06

漏洞nodejs代碼

2022-09-28 10:35:31

JavaScript代碼內(nèi)存泄漏
點(diǎn)贊
收藏

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