Vue.js 是一個流行且強大的 JavaScript 框架,它允許我們構建動態和交互式 Web 應用程序。
然而,與任何軟件一樣,Vue.js 應用程序有時會遇到內存泄漏,從而導致性能下降和意外行為。
今天,我們將深入探討 Vue.js 應用程序中內存泄漏的原因,並探索如何定位和修復這些問題的有效策略。
什麼是內存泄漏 ?
當程序執行過程中保留不再需要的內存時(主要是一些 變量、 方法等),會阻止內存被釋放並導致程序的內存使用量隨着時間的推移而增長,稱為內存泄漏。
在 Vue.js 應用程序中,內存泄漏通常是由於組件、全局 EventBus、事件定時器 和 變量,函數引用的管理不當而引起的。
1. EventBus 引起的內存泄露
一個老生常談的話題, Vue.js 中跨組件通信,要麼是EventBus, 要麼是Vuex 或者Pinia 這種數據流工具。
當我們對 EventBus 使用不當時,它們可能導致內存泄漏。
當組件被銷燬時,應將它們從事件總線中刪除,以防止延遲引用。
舉個例子:
// 組件A.vue
<template>
<div>
<button @click="sendMessage">廣播消息</button>
</div>
</template>
<script>
import { EventBus } from "./EventBus.js";
export default {
methods: {
sendMessage() {
EventBus.$emit("message", "Hello world from A!");
}
}
};
</script>
// 組件B.vue
<template>
<div>
<p>{{ receivedMessage }}</p>
</div>
</template>
<script>
import { EventBus } from "./EventBus.js";
export default {
data() {
return {
receivedMessage: ""
};
},
created() {
EventBus.$on("message", message => {
this.receivedMessage = message;
});
}
};
</script>
在此示例中,發生內存泄漏是因為 ComponentB 從EventBus訂閲了一個事件,但在該組件被銷燬時並未取消訂閲。
為了解決這個問題,我們需要在 組件B 的 beforeDestroy 鈎子中使用 EventBus.$off 來刪除事件監聽器。
所以 需要對組件B做如下修改:
// ComponentB.vue
<template>
<div>
<p>{{ receivedMessage }}</p>
</div>
</template>
<script>
import { EventBus } from "./EventBus.js";
export default {
data() {
return {
receivedMessage: ""
};
},
created() {
EventBus.$on("message", message => {
this.receivedMessage = message;
});
},
beforeDestroy() {
EventBus.$off("message"); //this line was missing previously
}
};
</script>
2. 未被清理的定時器
Vue.js 應用程序中內存泄漏的最常見原因之一是未能正確刪除定時器。當組件在其生命週期中使用了定時器,但並未合理的進行清除。
一旦組件被銷燬, 定時器會繼續引用該組件,從而防止其被垃圾收集。
舉個例子:
<template>
<div>
<button @click="startShow">開始演示</button>
<button @click="stopShow">停止演示</button>
</div>
</template>
<script>
export default {
data() {
return {
intervalId: null
};
},
methods: {
startLeak() {
this.intervalId = setInterval(() => {
// Simulate some activity
console.log("Interval running...");
}, 1000);
},
stopLeak() {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
};
</script>
在這個例子中,發生內存泄漏是因為單擊 “開始演示” 按鈕時創建了interval 定時,但在組件被銷燬時沒有正確清理它。
為了解決這個問題,我們需要在 beforeDestroy 生命週期中,對定時器進行清理。
所以最終的代碼將如下所示:
<template>
<div>
<button @click="startShow">開始演示</button>
<button @click="stopShow">停止演示</button>
</div>
</template>
<script>
export default {
data() {
return {
intervalId: null
};
},
methods: {
startLeak() {
this.intervalId = setInterval(() => {
// Simulate some activity
console.log("Interval running...");
}, 1000);
},
stopLeak() {
clearInterval(this.intervalId);
this.intervalId = null;
}
},
@diff new
beforeDestroy() {
clearInterval(this.intervalId); // This line is missing above
}
};
</script>
3. 第三方庫使用不當
第三方類庫使用不當,是內存泄漏的最常見原因。
這是由於組件清理不當造成的。這裏我使用 Choices.js 庫進行演示。
// cdn Choice Library
<link rel='stylesheet prefetch' href='https://joshuajohnson.co.uk/Choices/assets/styles/css/choices.min.css?version=3.0.3'>
<script src='https://joshuajohnson.co.uk/Choices/assets/scripts/dist/choices.min.js?version=3.0.3'></script>
// our component
<div id="app">
<button
v-if="showChoices"
@click="hide"
>Hide</button>
<button
v-if="!showChoices"
@click="show"
>Show</button>
<div v-if="showChoices">
<select id="choices-single-default"></select>
</div>
</div>
// Script
new Vue({
el: "#app",
data: function () {
return {
showChoices: true
}
},
mounted: function () {
this.initializeChoices()
},
methods: {
initializeChoices: function () {
let list = []
// 創造更多的選項,方便直觀的觀察到內存泄漏
for (let i = 0; i < 1000; i++) {
list.push({
label: "選項" + i,
value: i
})
}
new Choices("#choices-single-default", {
searchEnabled: true,
removeItemButton: true,
choices: list
})
},
show: function () {
this.showChoices = true
this.$nextTick(() => {
this.initializeChoices()
})
},
hide: function () {
this.showChoices = false
}
}
})
在上面的例子中,
我們加載了一個包含許多選項的下拉列表,然後使用帶有 v-if 指令的顯示/隱藏按鈕來添加它並將其從虛擬 DOM 中刪除。
此示例的問題在於 v-if 指令從 DOM 中刪除了父元素,但我們沒有清理 Choices.js 創建的額外 DOM 片段,從而導致內存泄漏。
我為大家做了一個在線的示例工程,便於大家去觀察內存佔用情況。
http://demolab.seanz.net/m-leak/index.html
當多點幾次 之後,會有明確的內存增加。 附上一張 差異圖。
初始化狀態:
多次點擊狀態:
識別內存泄漏
識別 Vue.js 應用程序中的內存泄漏可能具有挑戰性,因為它們通常表現為性能緩慢或隨着時間的推移內存消耗增加。沒有神奇的工具可以識別代碼的問題所在。
但是,大多數現代瀏覽器都提供內存分析工具,允許您拍攝應用程序隨時間的內存使用情況的快照。這些工具可以幫助您識別哪些對象消耗了過多的內存以及哪些組件沒有得到正確的垃圾收集。
Chrome 的 "Heap Snapshot" 等工具可以通過可視化對象引用及其內存消耗來提供對內存使用情況的詳細瞭解。
這可以幫助您更準確地查明內存泄漏的根源。
如何儘可能避免內存泄漏?
- 正確的使用計時器:確保在 Mounted 生命週期掛鈎期間添加 計時器,在組件的 beforeDestroy 鈎子中,進行清除。
- EventBus :當你在組件中使用EventBus時,一定要確保,在組件銷燬鈎子方法中 將其 從 EventBus 中刪除。
- 響應式數據清理:在 beforeDestroy 生命週期鈎子中,清理響應式數據屬性,以防止它們保留對已銷燬組件的引用。
- 第三方類庫:當使用在 Vue 之外操作 DOM 的其他 3rd 方庫時,通常會發生這些泄漏。要修復此類泄漏,請正確遵循庫文檔並採取適當的措施。
總結
Vue.js 應用程序中的內存泄漏和性能測試可能很難識別和解決,而且在快速交付的興奮中也很容易被忽視。
但是,保持較小的內存佔用對於整體用户體驗仍然很重要。
藉助正確的工具、技術和實踐,您可以顯着減少遇到它們的機會。
通過正確管理一些明顯引起內存泄漏的方法,您可以確保 Vue.js 應用程序以最佳性能運行並保持健康的內存佔用。