這是我參與8月更文挑戰的第5天,活動詳情查看:8月更文挑戰前端
這篇文章是針對瀏覽器的 JavaScript 腳本,Node.js 大同小異,這裏不涉及到 Node.js 的場景。固然 Node.js 做爲服務端語言,必然更關注內存泄漏的問題。vue
用戶通常不會在一個 Web 頁面停留過久,即便有一點內存泄漏,重載頁面內存也會跟着釋放。並且瀏覽器也有自動回收內存的機制,因此也不會特別關注內存泄漏的問題。程序員
可是做爲開發人員的咱們若是對內存泄漏沒有什麼概念,有時候仍是有可能由於內存泄漏,致使頁面卡頓。瞭解了內存泄漏,就知道該如何避免內存泄漏,這也是咱們提高前端技能的必經之路。web
內存是計算機中重要的部件之一,它是與CPU進行溝通的橋樑。計算機中全部程序的運行都是在內存中進行的,所以內存的性能對計算機的影響很是大。內存(Memory)也被稱爲[內部存儲器]其做用是用於暫時存放CPU中的運算數據,以及與[硬盤]等[外部存儲器]交換的數據。只要計算機在運行中,CPU就會把須要運算的數據調到內存中進行運算,當運算完成後CPU再將結果傳送出來,內存的運行也決定了計算機的穩定運行。 內存是由[內存芯片]、電路板、[金手指]等部分組成的。算法
內存也是有生命週期的,無論什麼程序語言,通常能夠按順序分爲三個週期:segmentfault
分配期數組
分配所須要的內存瀏覽器
使用期markdown
使用分配到的內存(讀、寫)函數
釋放期
不須要時將其釋放和歸還
內存分配 -> 內存使用 -> 內存釋放。
在計算機科學中,內存泄漏指因爲疏忽或錯誤形成程序未能釋放已經再也不使用的內存。內存泄漏並不是指內存在物理上的消失,而是應用程序分配某段內存後,因爲設計錯誤,致使在釋放該段內存以前就失去了對該段內存的控制,從而形成了內存的浪費。
若是內存不須要時,沒有通過生命週期的釋放期,那麼就存在內存泄漏。
內存泄漏簡單理解:無用的內存還在佔用,得不到釋放和歸還。比較嚴重時,無用的內存會持續遞增,從而致使整個系統卡頓,甚至崩潰。
像 C 語言這樣的底層語言通常都有底層的內存管理接口,好比
malloc()
和free()
。相反,JavaScript是在建立變量(對象,字符串等)時自動進行了分配內存,而且在不使用它們時「自動」釋放。 釋放的過程稱爲垃圾回收。這個「自動」是混亂的根源,並讓JavaScript(和其餘高級語言)開發者錯誤的感受他們能夠不關心內存管理。
JavaScript 內存管理機制和內存的生命週期是一一對應的。首先須要分配內存,而後使用內存,最後釋放內存。
其中 JavaScript 語言不須要程序員手動分配內存,絕大部分狀況下也不須要手動釋放內存,對 JavaScript 程序員來講一般就是使用內存(即便用變量、函數、對象等)。
JavaScript 定義變量就會自動分配內存的。咱們只需瞭解 JavaScript 的內存是自動分配的就足夠了。
看下內存自動分配的例子:
// 給數值變量分配內存
let number = 123;
// 給字符串分配內存
const string = "xianshannan";
// 給對象及其包含的值分配內存
const object = {
a: 1,
b: null
};
// 給數組及其包含的值分配內存(就像對象同樣)
const array = [1, null, "abra"];
// 給函數(可調用的對象)分配內存
function func(a){
return a;
}
複製代碼
使用值的過程其實是對分配內存進行讀取與寫入的操做。讀取與寫入多是寫入一個變量或者一個對象的屬性值,甚至傳遞函數的參數。
根據上面的內存自動分配例子,咱們繼續內存使用的例子:
// 寫入內存
number = 234;
// 讀取 number 和 func 的內存,寫入 func 參數內存
func(number);
複製代碼
前端界通常稱垃圾內存回收爲 GC
(Garbage Collection,即垃圾回收)。
內存泄漏通常都是發生在這一步,JavaScript 的內存回收機制雖然能回收絕大部分的垃圾內存,可是仍是存在回收不了的狀況,若是存在這些狀況,須要咱們手動清理內存。
之前一些老版本的瀏覽器的 JavaScript 回收機制沒那麼完善,常常出現一些 bug 的內存泄漏,不過如今的瀏覽器基本都沒這些問題了,已過期的知識這裏就不作深究了。
這裏瞭解下如今的 JavaScript 的垃圾內存的兩種回收方式,熟悉下這兩種算法能夠幫助咱們理解一些內存泄漏的場景。
這是最初級的垃圾收集算法。此算法把「對象是否再也不須要」簡化定義爲「對象有沒有其餘對象引用到它」。若是沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。
看下下面的例子,「這個對象」的內存被回收了嗎?
// 「這個對象」分配給 a 變量
var a = {
a: 1,
b: 2,
}
// b 引用「這個對象」
var b = a;
// 如今,「這個對象」的原始引用 a 被 b 替換了
a = 1;
複製代碼
當前執行環境中,「這個對象」內存尚未被回收的,須要手動釋放「這個對象」的內存(固然是還沒離開執行環境的狀況下),例如:
b = null;
// 或者 b = 1,反正替換「這個對象」就好了
複製代碼
這樣引用的"這個對象"的內存就被回收了。
ES6 把引用有區分爲強引用和弱引用,這個目前只有再 Set 和 Map 中才有。
強引用纔會有引用計數疊加,只有引用計數爲 0 的對象的內存纔會被回收,因此通常須要手動回收內存(手動回收的前提在於標記清除法還沒執行,還處於當前執行環境)。
而弱引用沒有觸發引用計數疊加,只要引用計數爲 0,弱引用就會自動消失,無需手動回收內存。
當變量進入執行環境時標記爲「進入環境」,當變量離開執行環境時則標記爲「離開環境」,被標記爲「進入環境」的變量是不能被回收的,由於它們正在被使用,而標記爲「離開環境」的變量則能夠被回收
環境能夠理解爲咱們的做用域,可是全局做用域的變量只會在頁面關閉纔會銷燬。
// 假設這裏是全局變量
// b 被標記進入環境
var b = 2;
function test() {
var a = 1;
// 函數執行時,a 被標記進入環境
return a + b;
}
// 函數執行結束,a 被標記離開環境,被回收
// 可是 b 就沒有被標記離開環境
test();
複製代碼
JavaScript 的內存回收機制雖然能回收絕大部分的垃圾內存,可是仍是存在回收不了的狀況。程序員要讓瀏覽器內存泄漏,瀏覽器也是管不了的。
下面有些例子是在執行環境中,沒離開當前執行環境,還沒觸發標記清除法。因此你須要讀懂上面 JavaScript 的內存回收機制,才能更好理解下面的場景。
// 在全局做用域下定義
function count(number) {
// basicCount 至關於 window.basicCount = 2;
basicCount = 2;
return basicCount + number;
}
複製代碼
不過在 eslint 幫助下,這種場景如今基本沒人會犯了,eslint 會直接報錯,瞭解下就好。
無用的計時器忘記清理是新手最容易犯的錯誤之一。
就拿一個 vue 組件來作例子。
<template>
<div></div>
</template>
<script>
export default {
methods: {
refresh() {
// 獲取一些數據
},
},
mounted() {
setInterval(function() {
// 輪詢獲取數據
this.refresh()
}, 2000)
},
}
</script>
複製代碼
上面的組件銷燬的時候,setInterval
仍是在運行的,裏面涉及到的內存都是無法回收的(瀏覽器會認爲這是必須的內存,不是垃圾內存),須要在組件銷燬的時候清除計時器,以下:
<template>
<div></div>
</template>
<script>
export default {
methods: {
refresh() {
// 獲取一些數據
},
},
mounted() {
this.refreshInterval = setInterval(function() {
// 輪詢獲取數據
this.refresh()
}, 2000)
},
beforeDestroy() {
clearInterval(this.refreshInterval)
},
}
</script>
複製代碼
無用的事件監聽器忘記清理是新手最容易犯的錯誤之一。
仍是繼續使用 vue 組件作例子。
<template>
<div></div>
</template>
<script>
export default {
mounted() {
window.addEventListener('resize', () => {
// 這裏作一些操做
})
},
}
</script>
複製代碼
上面的組件銷燬的時候,resize 事件仍是在監聽中,裏面涉及到的內存都是無法回收的(瀏覽器會認爲這是必須的內存,不是垃圾內存),須要在組件銷燬的時候移除相關的事件,以下:
<template>
<div></div>
</template>
<script>
export default {
mounted() {
this.resizeEventCallback = () => {
// 這裏作一些操做
}
window.addEventListener('resize', this.resizeEventCallback)
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeEventCallback)
},
}
</script>
複製代碼
文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊👍收藏加關注😊,但願點贊多多多多...