vue.js 内存泄漏的识别和解决方案
免责声明
渣翻译,仅供大家参考
毋庸置疑, .Vue.js是非常流行和强大的Javascript框架, 用来让我们构建动态和交互式的WEB应用,然而, 如同很多软件,Vue.js应用有时候会出现内存溢出从而导致卡顿和一些异常的行为,今天,我们将深入Vue.js应用内存泄漏的原因,并找出有效的方法来识别和修复这些问题
什么是内存泄漏
当一个程序已经很长时间不需要但是仍然意外的保持了内存,阻止了内存释放并且随着时间流逝内存持续增长的原因, 称之为内存泄漏, 在Vue.js应用中,比较典型的内存溢出出现在不正确的组件管理,全局event bus, 时间监听和传参
让我们通过一些例子来演示Vue.js应用的内存泄漏, 并修复它们
1.全局Event Bus泄漏
全局Event Bus用于组件之间的通信,如果不正确的管理则会导致内存泄漏,如果组件销毁,应该删除event bus从而阻止传参遗留
例如:
javascript
// EventBus.js
import Vue from "vue";
export const EventBus = new Vue();
// ComponentA.vue
<template>
<div>
<button @click="sendMessage">Send Message</button>
</div>
</template>
<script>
import { EventBus } from "./EventBus.js";
export default {
methods: {
sendMessage() {
EventBus.$emit("message", "Hello from Component A!");
}
}
};
</script>
// 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;
});
}
};
</script>
在这个例子中, 由于组件B从全局event bus订阅了一个事件, 但是在销毁的时候没有进行取消订阅, 所以出现了意外的内存泄漏, 修复这个问题, 我们需要在组件B的beforeDestroy声明周期中, 用EventBus.$off来删除事件订阅,所以组件B看起来如下:
javascript
// 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应用内存泄漏常见原因是没有删除事件监听, 当一个组件在他的生命周期中只注册了事件监听, 但没有移除他们, 当组件卸载, 监听器仍然继续引用组件, 从而导致阻止了事件的垃圾回收
例如:
javascript
<template>
<div>
<button @click="startLeak">Start Memory Leak</button>
<button @click="stopLeak">Stop Memory Leak</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>
这里, 因为点击Start Memory Leak创建了事件监听(interval),但是组件销毁的时候没有完全的删除, 所以出现了意外的内存泄漏, 要修正他, 我们需要在beforeDestroy钩子清除interval
最后的代码如下
javascript
<template>
<div>
<button @click="startLeak">Start Memory Leak</button>
<button @click="stopLeak">Stop Memory Leak</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;
}
},
beforeDestroy() {
clearInterval(this.intervalId); // This line is missing above
}
};
</script>
3.第三方库
内存泄漏常见原因,还出现在组件没有正确的清理,这里我用了Choices.js库来演示
javascript
// 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 = []
// loading many option to increate memory usage
for (let i = 0; i < 1000; i++) {
list.push({
label: "Item " + 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
}
}
})
在这个例子上, 我们加载了一个有大量选项的select下拉框, 并且通过Show/Hide按钮结合
v-if指令来添加删除他们的虚拟dom,这个的问题在于v-if指令从DOM中删除了父级组件, 但是我们没有清理Choices.js创建的附加DOM, 所以出现了内存泄漏
在Chrome浏览器中打开项目,到谷歌任务管理器,观察这个组件的内存使用情况,当你点击
Show/Hid按钮,每次点击当前页面的内存占用都会增长,当你停止点击, 也没有释放占用的内存
javascript
new Vue({
el: "#app",
data: function () {
return {
showChoices: true,
choicesSelect: null // creates a variable to for reference
}
},
mounted: function () {
this.initializeChoices()
},
methods: {
initializeChoices: function () {
let list = []
for (let i = 0; i < 1000; i++) {
list.push({
label: "Item " + i,
value: i
})
}
// Set a reference to our choicesSelect in our Vue instance
this.choicesSelect = new Choices("#choices-single-default", {
searchEnabled: true,
removeItemButton: true,
choices: list
})
},
show: function () {
this.showChoices = true
this.$nextTick(() => {
this.initializeChoices()
})
},
hide: function () {
// now we clean up reference
this.choicesSelect.destroy()
this.showChoices = false
}
}
})
这里有一个demo中谷歌任务管理的截图
点击Show/Hide按钮之前
点击50, 60次显示/隐藏两个tabs
获取这个问题以及解决办法的详细demo请访问我的GitHub Repo,clone项目, 并且在谷歌浏览器打开index.html你就可以运行
确认内存溢出
确认vue.js应用中的内存溢出是富有挑战的, 因为他们经常表现为卡顿或者占用内存越来越高,没有什么万能的工具能确认你的代码哪里出现了问题
然而,大多数现代浏览器提供了内存分析工具, 允许你创建应用内存使用的快照,这些工具可以帮助你确认哪些对象占用了额外的内存,哪些组件没有正确的垃圾回收
例如谷歌的Heap Snapshot(堆快照)就通过可视化对象应用和内存消耗来提供内存使用的详细分析,这些可以帮你准确的定位内存溢出的原因
修正vue.js应用中的内存溢出
1.正确的时间监听管理: 保证事件监听器在组件的mounted生命周期中添加, 在beforeDestroy生命周期中删除
2.解决循环引用的问题: 如果在组件之间创建循环引用一定要谨慎,如果这些循环引用是必要的,一定要确保组件销毁后终止引用
3.清除全局Event Bus:组件销毁时,在合适的生命周期钩子中删除全局Event Bus
4.清除响应式数据:使用beforeDestroy生命周期钩子清除响应式数据属性, 以阻止组件已经销毁了,但是还引用这些属性
5.第三方依赖库:有些三方库在vue之外操作dom经常出现意外泄漏, 要修复这种类型的泄露,需要根据文档指引采取恰当的措施
结论
在快速交付的vue.js应用中, 内存溢出和性能测试是比较难以发现和容易忽视的
然而,保持最小的内存占用对于整体用户体验是非常重要的
用正确的工具,正确的技术方案,正确的开发习惯, 你可以显著的减少遇到他们的可能性.正确的管理事件监听,循环引用,全局event buses, 和响应式数据,你可以保证你的vue.js应用稳定运行并保持健康的内存占用
Bonus:
为什么计算机需要治疗?
因为他有很多内存问题要解决!😄
Happy Coding! 🚀🚀