「硬核JS」你的程序中可能存在內存泄漏

聲明:本文爲掘金首發簽約文章,未經受權禁止轉載。javascript

寫在前面

想來不少同窗看到內存泄漏,心裏直接會跳出兩個字:閉包!!!再讓你說點其它的估計就噤聲了。若是你對內存泄漏的瞭解僅限於閉包,那真的是應該仔細看此文了,閉包可能會形成內存泄漏,可是內存泄漏並非只有閉包,它只是內存泄漏的引子之一罷了。html

寫的程序運行一段時間後慢慢變卡甚至要崩潰了?前端

如題,你的程序中可能存在內存泄漏,說到內存泄漏,建議先讀 「硬核JS」你真的懂垃圾回收機制嗎 一文,而後再來看此文會比較通透,畢竟垃圾回收和內存泄漏是因果關係,垃圾被回收了啥事沒有,垃圾沒被回收就是內存泄漏。java

此文咱們會介紹內存泄漏的相關概念和引發內存泄漏的一些問題,還會着重給你們介紹內存泄漏的排查、定位及修復方法(學到便可用到),最後還簡單擴展了下前端內存三大件的其餘兩件內存膨脹和頻繁 GC 的概念。node

什麼是內存泄漏

引擎中有垃圾回收機制,它主要針對一些程序中再也不使用的對象,對其清理回收釋放掉內存。程序員

那麼垃圾回收機制會把再也不使用的對象(垃圾)全都回收掉嗎?正則表達式

其實引擎雖然針對垃圾回收作了各類優化從而儘量的確保垃圾得以回收,但並非說咱們就能夠徹底不用關心這塊了,咱們代碼中依然要主動避免一些不利於引擎作垃圾回收操做,由於不是全部無用對象內存均可以被回收的,那當再也不用到的對象內存,沒有及時被回收時,咱們叫它 內存泄漏(Memory leak)數組

常見的內存泄漏

代碼不規範,同事兩行淚,接下來咱們看看會引發內存泄漏的一些常見案例。瀏覽器

不正當的閉包

閉包就是函數內部嵌套並 return 一個函數???這是大多數人認爲的閉包,好吧,它確實也是,咱們來看看幾本 JS 高光書中的描述:緩存

  • JavaScript高級程序設計:閉包是指有權訪問另外一個函數做用域中的變量的函數
  • JavaScript權威指南:從技術的角度講,全部的JavaScript函數都是閉包:它們都是對象,它們都關聯到做用域鏈
  • 你不知道的JavaScript:當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用域以外執行

按照上面三本書中的描述,那閉包所涉及的的範圍就比較廣了,咱們這裏暫時不去糾結閉包的定義,就以最簡單、你們都承認的閉包例子來看閉包:

function fn1(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log('hahaha')
  }
}
let fn1Child = fn1()
fn1Child()
複製代碼

上例是閉包嗎?它形成內存泄漏了嗎?

顯然它是一個典型閉包,可是它並無形成內存泄漏,由於返回的函數中並無對 fn1 函數內部的引用,也就是說,函數 fn1 內部的 test 變量徹底是能夠被回收的,那咱們再來看:

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
複製代碼

上例是閉包嗎?它形成內存泄漏了嗎?

顯然它也是閉包,而且由於 return 的函數中存在函數 fn2 中的 test 變量引用,因此 test 並不會被回收,也就形成了內存泄漏。

那麼怎樣解決呢?

其實在函數調用後,把外部的引用關係置空就行了,以下:

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
fn2Child = null
複製代碼

「 減小使用閉包,閉包會形成內存泄漏。。。 」

醒醒,這句話是過去式了,它的描述不許確,So,應該說不正當的使用閉包可能會形成內存泄漏。

隱式全局變量

咱們知道 JavaScript 的垃圾回收是自動執行的,垃圾回收器每隔一段時間就會找出那些再也不使用的數據,並釋放其所佔用的內存空間。

再來看全局變量和局部變量,函數中的局部變量在函數執行結束後這些變量已經再也不被須要,因此垃圾回收器會識別並釋放它們。可是對於全局變量,垃圾回收器很難判斷這些變量何時纔不被須要,因此全局變量一般不會被回收,咱們使用全局變量是 OK 的,但同時咱們要避免一些額外的全局變量產生,以下:

function fn(){
  // 沒有聲明從而製造了隱式全局變量test1
  test1 = new Array(1000).fill('isboyjc1')
  
  // 函數內部this指向window,製造了隱式全局變量test2
  this.test2 = new Array(1000).fill('isboyjc2')
}
fn()
複製代碼

調用函數 fn ,由於 沒有聲明 和 函數中this 的問題形成了兩個額外的隱式全局變量,這兩個變量不會被回收,這種狀況咱們要儘量的避免,在開發中咱們可使用嚴格模式或者經過 lint 檢查來避免這些狀況的發生,從而下降內存成本。

除此以外,咱們在程序中也會不可避免的使用全局變量,這些全局變量除非被取消或者從新分配以外也是沒法回收的,這也就須要咱們額外的關注,也就是說當咱們在使用全局變量存儲數據時,要確保使用後將其置空或者從新分配,固然也很簡單,在使用完將其置爲 null 便可,特別是在使用全局變量作持續存儲大量數據的緩存時,咱們必定要記得設置存儲上限並及時清理,否則的話數據量愈來愈大,內存壓力也會隨之增高。

var test = new Array(10000)

// do something

test = null
複製代碼

遊離DOM引用

考慮到性能或代碼簡潔方面,咱們代碼中進行 DOM 時會使用變量緩存 DOM 節點的引用,但移除節點的時候,咱們應該同步釋放緩存的引用,不然遊離的子樹沒法釋放。

<div id="root">
  <ul id="ul">
    <li></li>
    <li></li>
    <li id="li3"></li>
    <li></li>
  </ul>
</div>
<script> let root = document.querySelector('#root') let ul = document.querySelector('#ul') let li3 = document.querySelector('#li3') // 因爲ul變量存在,整個ul及其子元素都不能GC root.removeChild(ul) // 雖置空了ul變量,但因爲li3變量引用ul的子節點,因此ul元素依然不能被GC ul = null // 已無變量引用,此時能夠GC li3 = null </script>

複製代碼

如上所示,當咱們使用變量緩存 DOM 節點引用後刪除了節點,若是不將緩存引用的變量置空,依然進行不了 GC,也就會出現內存泄漏。

假如咱們將父節點置空,可是被刪除的父節點其子節點引用也緩存在變量裏,那麼就會致使整個父 DOM 節點樹下整個遊離節點樹均沒法清理,仍是會出現內存泄漏,解決辦法就是將引用子節點的變量也置空,以下圖:

遺忘的定時器

程序中咱們常常會用到計時器,也就是 setTimeoutsetInterval,先來看一個例子:

// 獲取數據
let someResource = getData()
setInterval(() => {
  const node = document.getElementById('Node')
	if(node) {
    node.innerHTML = JSON.stringify(someResource))
	}
}, 1000)
複製代碼

上面是我隨便 copy 的一個小例子,其代碼中每隔一秒就將獲得的數據放入到 Node 節點中去,可是在 setInterval 沒有結束前,回調函數裏的變量以及回調函數自己都沒法被回收。

什麼才叫結束呢?也就是調用了 clearInterval。若是沒有被 clear 掉的話,就會形成內存泄漏。不只如此,若是回調函數沒有被回收,那麼回調函數內依賴的變量也無法被回收。因此在上例中,someResource 就無法被回收。

一樣,setTiemout 也會有一樣的問題,因此,當不須要 interval 或者 timeout 時,最好調用 clearInterval 或者 clearTimeout來清除,另外,瀏覽器中的 requestAnimationFrame 也存在這個問題,咱們須要在不須要的時候用 cancelAnimationFrame API 來取消使用。

遺忘的事件監聽器

當事件監聽器在組件內掛載相關的事件處理函數,而在組件銷燬時不主動將其清除時,其中引用的變量或者函數都被認爲是須要的而不會進行回收,若是內部引用的變量存儲了大量數據,可能會引發頁面佔用內存太高,這樣就形成意外的內存泄漏。

咱們就拿 Vue 組件來舉例子,React 裏也是同樣的:

<template>
  <div></div>
</template>

<script> export default { created() { window.addEventListener("resize", this.doSomething) }, beforeDestroy(){ window.removeEventListener("resize", this.doSomething) }, methods: { doSomething() { // do something } } } </script>
複製代碼

遺忘的監聽者模式

監聽者模式想必咱們都知道,無論是 Vue 、 React 亦或是其餘,對於目前的前端開發框架來講,監聽者模式實現一些消息通訊都是很是常見的,好比 EventBus. . .

當咱們實現了監聽者模式並在組件內掛載相關的事件處理函數,而在組件銷燬時不主動將其清除時,其中引用的變量或者函數都被認爲是須要的而不會進行回收,若是內部引用的變量存儲了大量數據,可能會引發頁面佔用內存太高,這樣也會形成意外的內存泄漏。

仍是用 Vue 組件舉例子,由於比較簡單:

<template>
  <div></div>
</template>

<script> export default { created() { eventBus.on("test", this.doSomething) }, beforeDestroy(){ eventBus.off("test", this.doSomething) }, methods: { doSomething() { // do something } } } </script>
複製代碼

如上,咱們只需在 beforeDestroy 組件銷燬生命週期裏將其清除便可。

遺忘的Map、Set對象

當使用 MapSet 存儲對象時,同 Object 一致都是強引用,若是不將其主動清除引用,其一樣會形成內存不自動進行回收。

若是使用 Map ,對於鍵爲對象的狀況,能夠採用 WeakMapWeakMap 對象一樣用來保存鍵值對,對於鍵是弱引用(注:WeakMap 只對於鍵是弱引用),且必須爲一個對象,而值能夠是任意的對象或者原始值,因爲是對於對象的弱引用,不會干擾 Js 的垃圾回收。

若是須要使用 Set 引用對象,能夠採用 WeakSetWeakSet 對象容許存儲對象弱引用的惟一值,WeakSet 對象中的值一樣不會重複,且只能保存對象的弱引用,一樣因爲是對於對象的弱引用,不會干擾 Js 的垃圾回收。

這裏可能須要簡單介紹下,談弱引用,咱們先來講強引用,以前咱們說 JS 的垃圾回收機制是若是咱們持有對一個對象的引用,那麼這個對象就不會被垃圾回收,這裏的引用,指的就是 強引用 ,而弱引用就是一個對象若只被弱引用所引用,則被認爲是不可訪問(或弱可訪問)的,所以可能在任什麼時候刻被回收。

不明白?來看例子就曉得了:

// obj是一個強引用,對象存於內存,可用
let obj = {id: 1}

// 重寫obj引用
obj = null 
// 對象從內存移除,回收 {id: 1} 對象
複製代碼

上面是一個簡單的經過重寫引用來清除對象引用,使其可回收。

再看下面這個:

let obj = {id: 1}
let user = {info: obj}
let set = new Set([obj])
let map = new Map([[obj, 'hahaha']])

// 重寫obj
obj = null 

console.log(user.info) // {id: 1}
console.log(set)
console.log(map)
複製代碼

此例咱們重寫 obj 之後,{id: 1} 依然會存在於內存中,由於 user 對象以及後面的 set/map 都強引用了它,Set/Map、對象、數組對象等都是強引用,因此咱們仍然能夠獲取到 {id: 1} ,咱們想要清除那就只能重寫全部引用將其置空了。

接下來咱們看 WeakMap 以及 WeakSet

let obj = {id: 1}
let weakSet = new WeakSet([obj])
let weakMap = new WeakMap([[obj, 'hahaha']])

// 重寫obj引用
obj = null

// {id: 1} 將在下一次 GC 中從內存中刪除
複製代碼

如上所示,使用了 WeakMap 以及 WeakSet 即爲弱引用,將 obj 引用置爲 null 後,對象 {id: 1} 將在下一次 GC 中被清理出內存。

未清理的Console輸出

寫代碼的過程當中,確定避免不了一些輸出,在一些小團隊中可能項目上線也不清理這些 console,卻不知這些 console 也是隱患,同時也是容易被忽略的,咱們之因此在控制檯能看到數據輸出,是由於瀏覽器保存了咱們輸出對象的信息數據引用,也正是所以未清理的 console 若是輸出了對象也會形成內存泄漏。

因此,開發環境下咱們可使用控制檯輸出來便於咱們調試,可是在生產環境下,必定要及時清理掉輸出。

可能有同窗會以爲難以想象,甚至不相信,這裏咱們留一個例子,你們看完文章恰好能夠本身測試一下,能夠先保存這段代碼哦!(如何測試看完下文就明白啦)

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>test</title>
</head>

<body>
  <button id="click">click</button>

  <script> !function () { function Test() { this.init() } Test.prototype.init = function () { this.a = new Array(10000).fill('isboyjc') console.log(this) } document.querySelector('#click').onclick = function () { new Test(); } }() </script>
</body>

</html>
複製代碼

內存泄漏排查、定位與修復

正如開頭所說,程序運行一段時間後慢慢變卡甚至要崩潰了,不知道是什麼緣由,那咱們就經過一個例子來走一遍排查、定位以及修復內存泄漏的整個流程,敲黑板,這是你們真正可以用上的。

既然上面咱們說了幾個會形成內存泄漏的案例,那咱們就用這些案例寫個 Demo 來從瀏覽器的角度反推排查是否存在內存泄漏,存在的話定位泄漏源並給予修復。

首先,咱們來捏造一個內存泄漏例子:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>test</title>
</head>

<body>
  <button id="click">click</button>
  <h1 id="content"></h1>

  <script> let click = document.querySelector("#click"); let content = document.querySelector("#content") let arr = [] function closures() { let test = new Array(1000).fill('isboyjc') return function () { return test } } click.addEventListener("click", function () { arr.push(closures()) arr.push(closures()) content.innerHTML = arr.length }); </script>
</body>

</html>
複製代碼

如上所示,這是一個很是簡單的由不正當使用閉包構成的內存泄漏例子。

咱們先來簡單介紹下,只看 script 中的 JS 代碼便可,首先,咱們有一個 closures 函數,這是一個閉包函數,最簡單的閉包函數想必不用向你們介紹了吧,而後咱們爲頁面中的 button 元素綁定了一個點擊事件,每次點擊都將執行 2 次閉包函數並將其執行結果 push 到全局數組 arr 中,因爲閉包函數執行結果也是一個函數而且存在對原閉包函數內部數組 test 的引用,因此 arr 數組中每一項元素都使得其引用的閉包內部 test 數組對象沒法回收,arr 數組有多少元素,也就表明着咱們存在多少次閉包引用,因此此程序點擊次數越多,push 的越多,內存消耗越大,頁面也會愈來愈卡。

那爲了便於後期觀察,程序中咱們在每次點擊按鈕後,都把全局數組 arr 的長度數據更新到了頁面上,即從 0 開始,每點擊一次,頁面數值加 2。

固然,這是咱們本身寫的例子,做爲上帝的咱們知道是什麼緣由致使的,那如今,忘掉這些,假設這是咱們的一個項目程序,開發完成交付給測試,測試小姐姐發如今程序中不斷點擊按鈕後頁面愈來愈遲鈍了,隨即提了BUG。

做爲程序員的咱們確定是:「刷新下頁面不就行了,卡了就刷新刷新!!!」

嗯。。。產品和測試確定都不會答應,一句用戶至上就讓咱們改。。

行吧,那就改,首先第一步就要排查是哪裏出了問題、是什麼引發的,那此環節咱們就叫排查問題階段好了。

排查問題

Chrome 的開發者工具也就是咱們所說的瀏覽器控制檯(Chrome Devtool )功能其實十分強大,經過它能夠幫助咱們分析程序中像性能、安全、網絡等各類東西,也可讓咱們快速定位到問題源,只是大多數人並不熟悉其使用而已。

因爲此文咱們之內存泄漏爲主,那咱們就默認上述程序已經排查了除內存以外全部項且都沒問題,接下來開始排查內存這塊。

首先咱們開啓瀏覽器的無痕模式,接着打開要檢查的網頁程序代碼,而後打開控制檯,整個程序界面很是簡單,以下圖:

接着咱們找到 Performance 這一面板,以前叫 Timeline ,它是 Chrome Devtool 用來監控性能指標的一個利器,能夠記錄並分析在網站的生命週期內所發生的各種事件,咱們就能夠經過它監控咱們程序中的各類性能狀況並分析,其中就包括內存,以下圖:

接下來開始操做,在開始以前必定要確認勾選了 Memory 選項也就是上圖標記 5 ,這樣咱們才能夠看到內存相關的分析。

點擊開始錄製(標記 1)進入錄製狀態,隨後先清理一下GC,也就是點擊小垃圾桶(標記 6)。

接着瘋狂點擊頁面中 click 按鈕 100 次,這時頁面上的數值應該是 200,咱們再點擊一下小垃圾桶,手動觸發一次 GC。

再次瘋狂點擊頁面中 click 按鈕 100 次,這時頁面上的數值應該是 400,而後中止錄製。

咱們來觀察控制檯生成的數據面板,以下圖:

上面圈紅的兩塊,也就是 Heap 對應的部分表示內存在週期性的回落,簡單說就是咱們的內存狀況。

咱們能夠很明顯的看到,內存數據呈現出一個不斷上漲的趨勢,可能有人會說這段時間內是否是還沒執行 GC 呢?別急,還記得咱們在 200 的時候點擊了一下小垃圾桶嗎,也就是咱們中間手動觸發垃圾回收一次,咱們就能夠經過上面的頁面快照找出當頁面值爲 200 的那一刻在哪裏,很簡單,鼠標移到內存快照上找就好了,以下圖:

能夠看到,即便咱們中間手動作了一次垃圾回收操做,但清理後的內存並無減小不少,由此咱們推斷,此程序的點擊操做可能存在內存泄漏。

OK,排查到問題了,那接下來就是定位泄漏源在哪了。

你可能會說,既然已經找到問題所在就是點擊事件了,直接去改不就完了?

要知道,這是咱們寫的一個簡單的例子,咱們一會兒就能夠看出問題在哪,可是真實項目中一個點擊事件裏就可能存在大量操做,而咱們只知道點擊事件可能致使了內存泄漏,但不知道具體問題是在點擊事件的哪一步驟上,更加細粒度的引發緣由和位置咱們也不知,這些都還須要咱們進一步分析去定位。

分析定位

接下來咱們開始分析定位泄漏源

Chrome Devtool 還爲咱們提供了 Memory 面板,它能夠爲咱們提供更多詳細信息,好比記錄 JS CPU 執行時間細節、顯示 JS 對象和相關的DOM節點的內存消耗、記錄內存的分配細節等。

其中的 Heap Profiling 能夠記錄當前的堆內存 heap 的快照,並生成對象的描述文件,該描述文件給出了當下 JS 運行所用的全部對象,以及這些對象所佔用的內存大小、引用的層級關係等等,用它就能夠定位出引發問題的具體緣由以及位置。

注意,可不是 Performance 面板下那個 Memory ,而是與 Performance 面板同級的 Memory 面板,以下圖:

如今頁面值爲 400,固然也能夠刷新一下頁面從 0 開始,這裏咱們選擇繼續操做

首先點擊一下小垃圾桶(標記 3),觸發一下 GC,把沒用的東西從內存中幹掉

點擊開始生成快照(標記 1),生成第一次快照並選中,以下圖:

簡單介紹小上圖大概表示的什麼意思:

左側列表中的 Snapshot 1 表明了咱們生成的快照1,也就是剛剛那一刻的內存狀態

選中 Snapshot 1 後就是右側視圖表格了,表格左上方有一個下拉框,它有四個值

  • Summary:按照構造函數進行分組,捕獲對象和其使用內存的狀況,可理解爲一個內存摘要,用於跟蹤定位DOM節點的內存泄漏
  • Comparison:對比某個操做先後的內存快照區別,分析操做先後內存釋放狀況等,便於確認內存是否存在泄漏及形成緣由
  • Containment:探測堆的具體內容,提供一個視圖來查看對象結構,有助分析對象引用狀況,可分析閉包及更深層次的對象分析
  • Statistics:統計視圖

該下拉默認會爲咱們選擇 Summary ,因此下方表格展現的就是快照1中數據的內存摘要,簡單理解就是快照1生成的那一刻,內存中都存了什麼,包括佔用內存的信息等等。

來簡單瞭解下 Summary 選項數據表格的列都表示什麼

  • Constructor:顯示全部的構造函數,點擊每個構造函數能夠查看由該構造函數建立的全部對象
  • Distance:顯示經過最短的節點路徑到根節點的距離,引用層級
  • Shallow Size:顯示對象所佔內存,不包含內部引用的其餘對象所佔的內存
  • Retained Size:顯示對象所佔的總內存,包含內部引用的其餘對象所佔的內存

OK,暫時知道這麼多就能夠了,咱們繼續操做,先點擊小垃圾桶手動執行一次GC,而後點擊 1 下頁面的 click 按鈕,最後再次點擊生成快照按鈕,生成咱們的第二次快照。

爲了準確無誤,咱們多來幾回操做,以下:

先點擊小垃圾桶手動執行一次 GC,而後點擊 2 下頁面的 click 按鈕,最後再次點擊生成快照按鈕,生成咱們的第三次快照

先點擊小垃圾桶手動執行一次 GC,而後點擊 3 下頁面的 click 按鈕,最後再次點擊生成快照按鈕,生成咱們的第四次快照

隨後,咱們選中快照2,並將其上面的下拉框由默認的 Summary 選項切換爲 comparison 選項,也就是對比當前快照與以前一次快照的內存區別,以下圖:

咱們再來看看選擇 Comparison 下拉後,下方的表格列表明着什麼,這裏介紹幾個重要的

  • New:新建了多少個對象
  • Deleted:回收了多少個對象
  • Delta:新建的對象數 減去 回收的對象數

誒,到這咱們就有點那味兒了,咱們須要重點關注 Delta ,只要它是正數就可能存在問題,貼心的控制檯都已經給咱們排好序了,最上面的幾個咱們依次看就能夠。

固然,咱們還須要知道這每一行的數據都表明的是什麼,注意力轉移到 Constructor 這一列,咱們也說過,此列是構造函數,每個構造函數點擊均可以查看由該構造函數建立的全部對象,仍是要先介紹下此列中常見的構造函數大體表明什麼

  • system、system/Context 表示引擎本身建立的以及上下文建立的一些引用,這些不用太關注,不重要
  • closure 表示一些函數閉包中的對象引用
  • array、string、number、regexp 這一系列也能看出,就是引用了數組、字符串、數字或正則表達式的對象類型
  • HTMLDivElement、HTMLAnchorElement、DocumentFragment等等這些其實就是你的代碼中對元素的引用或者指定的 DOM 對象引用

誒,又清晰了不少,那接下來咱們就能夠依次對比 1->2 / 2->3 / 3->4 來看到底哪裏有問題了。

彆着急,想一下如今的咱們要怎麼作?須要單獨的點擊一個快照再選中 comparison ,而後看 Delta 列爲正數的項再進行分析,這樣的操做須要進行 3 次,由於咱們有 4 個快照,須要對比分析 3 次,甚至有時候可能生成的快照更多以此來確保準確性。

有沒有更簡單的方式呢?有的,咱們能夠直接選中要對比的快照,右側表格上還有一個彈框咱們能夠直接選擇快照進行對比,而且還會貼心的爲咱們過濾掉一些沒用的信息:

咱們來進行實際操做,左側選中快照2,選擇 快照1 快照2 進行對比分析,結果以下:

能夠看到,列表中只剩下對比過濾後的 4 項差別

system/Context 咱們無需關心。

closure 上面也說過表明閉包引用,咱們點擊此項看一下具體的信息:

能夠看到, closure 下有兩個引用,還貼心的爲咱們指出了在代碼的 21 行,點擊選中其中一個引用,下方還有更詳細的信息展現。

爲何展開後是兩個引用?還記得咱們在生成 快照2 時的操做嗎,手動執行了一次 GC 並點擊了一次 click 按鈕,觸發了一次點擊事件,點擊事件中咱們執行並 push 了兩次閉包函數,因此就是 2 條記錄。

最後咱們看 array ,這裏存在數組的引用是徹底由於咱們案例代碼中那個全局數組變量 arr 的存在,畢竟每次點擊都 push 數據呢,這也是咱們上面提到的爲何要額外關注全局變量的使用、要將它及時清理什麼的,就是由於像這種狀況你不清理的話這些全局變量在頁面關閉前就一直在內存裏,可能你們對構造函數列中有 2 項都是數組有疑問,其實沒毛病,一項表明的是 arr 自己,一項表明的是閉包內部引用的數組變量 test (忘了的話回顧一下上面案例代碼),這點也能夠經過 Distance 列中表示的引用層級來 GET,一個層級是 7,一個層級是 8。至於數組引發泄漏的代碼位置咱們也能夠點擊展開並選中其引用條目,詳情裏就能夠看到代碼位置,同上面閉包同樣的操做,這裏就不演示了。

誒,那好像就知道具體的泄漏源了,咱們再次證明一下,左側選中快照4,選擇 快照3快照4 進行對比分析,快照4 前咱們作的操做是手動執行了一次 GC 並點擊了三次 click 按鈕,若是上述結論正確的話,應該同咱們上面 快照1快照2 對比結果的數據項一致都是 4 項,可是每項的內部引用會是 6 條,由於此次快照前咱們點擊了三次按鈕,每次執行並 push 了兩次閉包函數,來看結果:

image-20210707041841176

嗯,到這裏一切好像變得清晰明朗了,問題一共有 2 個,一是代碼 21 行的閉包引用數組形成的內存泄漏,二是全局變量 arr 的元素不斷增多形成的內存泄漏。

分析定位成功,進入下一步驟,修復並再次驗證。

修復驗證

因爲這是臨時寫的一個案例,沒有具體的場景,因此也就沒有辦法使用針對性的方式來修復,So,此步驟暫時忽略,不過在項目中咱們仍是要解決的。

好比全局對象一直增大這個問題,全局對象咱們沒法避免,可是能夠限制一下全局對象的大小,根據場景能夠超出就清理一部分。

好比閉包引用的問題,不讓它引用,或者執行完置空,這都是上面說過的。

總之,一切都須要根據具體場景選擇解決方案,解決以後重複上面排查流程看內存便可。

內存三大件

其實前端關於內存方面主要有三個問題,我把它們親切的稱做內存三大件:

內存泄漏 咱們說好久了,對象已經再也不使用但沒有被回收,內存沒有被釋放,即內存泄漏,那想要避免就避免讓無用數據還存在引用關係,也就是多注意咱們上面說的常見的幾種內存泄漏的狀況。

內存膨脹 即在短期內內存佔用極速上升到達一個峯值,想要避免須要使用技術手段減小對內存的佔用。

頻繁 GC 同這個名字,就是 GC 執行的特別頻繁,通常出如今頻繁使用大的臨時變量致使新生代空間被裝滿的速度極快,而每次新生代裝滿時就會觸發 GC,頻繁 GC 一樣會致使頁面卡頓,想要避免的話就不要搞太多的臨時變量,由於臨時變量不用了就會被回收,這和咱們內存泄漏中說避免使用全局變量衝突,其實,只要把握好其中的度,不太過度就 OK。

最後

若是你的程序運行出現卡頓卻找不到緣由,那不妨試試本文的排查方法吧,說不定就是內存泄漏引發的,同時,它也是咱們在作頁面優化時須要額外注意的一個點。

今天就到這裏了,你 GET 到了嗎?歡迎指誤堪錯!寫做不易,動動小手點個贊吧,收藏吃灰是大忌 👊

也歡迎關注 「硬核JS」 專欄,一文深刻介紹一個 JS 小知識,讓你知道你不知道的 JavaScript!!!

相關文章
相關標籤/搜索