写在前面:
在平时写代码时,内存泄漏的情况会时有发生,虽然js有内存回收机制,但在平时编程中还是需要注意避免内存泄漏的情况;前几天做移动端时遇到一个内存泄漏造成移动端页面卡顿的问题,所以想总结下前端内存泄漏的情况,回顾下基础知识
一、什么是内存泄漏
程序运行时操作系统会分配相应的内存,如果不进行定时的清理内存的占用情况,内存占用越来越高,很容易造成页面卡顿,进程奔溃;如果程序在系统分配了内存空间后不再使用但是没有及时释放就会造成内存泄漏;程序向系统申请的内存空间超出了系统能给的,就造成了内存溢出。内存泄漏和溢出都会影响程序的性能。
js不需要手动给变量申请内存,当我们在申明一个变量时,js会自动为其分配内存;当某个对象没有被引用会进行回收,最简单的垃圾回收机制是引用计数,当某个对象被引用的次数达到0时就会被回收
二、常见的造成内存泄漏的情况
1.全局变量:一个变量被挂载到window上,那么它永远都是可达的,只有关闭页面或关闭浏览器时被回收。严格模式下可以避免这个情况;解决方法:testVal = null
局部变量:在函数执行过后,局部变量就被回收了,此时便可以将它引用的内存释放掉
另一种全局变量可能由this创建
javascript">function foo() {
this.variable = "potential a***idental global";
}
// foo 调用自己,此时this 指向了全局对象(window)
foo();
2.定时器
使用定时器时,我们销毁了这个DOM,但是在定时器中使用了这个DOM,定时器中就保留了对这个DOM的引用,所以需要在清除DOM时也要手动清除定时器(timer = null)
var someResource = getData();
const timer = setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
解决:document.body.removeChild(node);someResource = null
timer = null ,这样定时器中的node 和someResource才会真的被回收掉
3.闭包函数导致的泄漏
闭包可以维持函数内部的局部变量,使其变量得不到释放
function bindEvent() {
var obj = document.createElement('XXX');
var unused = function () {
console.log(obj, '闭包内引用obj obj不会被释放');
};
obj = null; // 解决方法
}
4.DOM元素的事件监听
btn.addEventListener('click', onClick)
btn.removeEventListener('click', onClick)
5.没有清理对DOM元素的引用
const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用,能console出整个div 没有被回收
refA = null; // 这里会报错 Assignment to constant variable(赋值给常数变量).因为这个refA是const声明的,值不可以改变,将const改为var
console.log(refA, 'refA'); // 解除引用
三、Vue中占用内存的几种情况
1.全局变量在切换页面时没有清空
mounted() {
window.test = {
// 此处在全局window对象中引用了本页面的dom对象
name: 'home',
node: document.getElementById('home'),
}
},
解决:在页面卸载时顺便清空该引用
destroyed(){
window.test = null
}
2.监听在window或body上的事件没有解绑
- 如果在mounted/created 钩子中绑定了DOM/BOM 对象中的事件,需要在beforeDestroy 中做对应解绑处理
- 如果在mounted/created 钩子中使用了第三方库初始化,需要在beforeDestroy 中做对应销毁处理
- 如果组件中使用了定时器,需要在beforeDestroy 中做对应销毁处理
- 某些组件在模板中使用 事件绑定可能会出现泄漏,使用$on 替换模板中的绑定
如果是要render函数,避免在html标签中DOM / BOM事件
mounted () {
window.addEventListener('resize', this.func)
},
methods:{
func(){}
}
// 解决:
beforeDestroy () {
window.removeEventListener('resize', this.func)
}
3.在进入页面时打开弹框dialog,在离开页面时没有设为false
mounted () {
this.Dialog = true
},
// 在手动点击头部的返回按钮和回到首页的按钮时,会造成页面卡顿,无法滑动的问题
// 在离开页面时将它设为false
beforeRouteLeave(to, from, next){
this.Dialog = false
next()
},
4.绑在EventBus的事件没有解绑
解决:页面卸载时解除引用
mounted () {
this.$EventBus.$on('homeTask', res => this.func(res))
},
destroyed () {
this.$EventBus.$off()
}
5.v-if指令产生的内存泄漏
在使用v-if来控制显示隐藏的时候,当v-if的条件为false时,浏览器不会渲染这个元素;但是当我们在v-if为true时,给这个元素内添加了其他子元素,那么我们在设为false的时候只是这个父元素本身从虚拟DOM中移除它,新添加的子元素并没有移除,子元素对父元素引用了,父元素也就不会被移除
例子:
<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>
原来的写法
<script>
export default {
data() {
return {
showChoices: true,
}
},
mounted: function () {
this.initializeChoices()
},
methods: {
initializeChoices: function () {
let list = []
// 我们来为选择框载入很多选项,这样的话它会占用大量的内存
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
},
},
}
</script>
处理后的写法:我们在hide方法里移除父元素时对子元素做一个清理
<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>
export default {
data() {
return {
showChoices: true,
choicesSelect: null
}
},
mounted: function () {
this.initializeChoices()
},
methods: {
initializeChoices: function () {
let list = []
for (let i = 0; i < 1000; i++) {
list.push({
label: 'Item ' + i,
value: i,
})
}
// 在我们的 Vue 实例的数据对象中设置一个 `choicesSelect` 的引用
this.choicesSelect = new Choices("#choices-single-default", {
searchEnabled: true,
removeItemButton: true,
choices: list,
})
},
show: function () {
this.showChoices = true
this.$nextTick(() => {
this.initializeChoices()
})
},
hide: function () {
// 现在我们可以让 Choices 使用这个引用,从 DOM 中移除这些元素之前进行清理工作
this.choicesSelect.destroy()
this.showChoices = false
},
},
}
</script>
6.echart图表带来的内存泄漏
每一个图例在没有数据的时候它会创建一个定时器去渲染气泡,页面切换后,echarts 图例是销毁了,但是这个 echarts 的实例还在内存当中,同时它的气泡渲染定时器还在运行。这就导致 Echarts 占用 CPU 高,导致浏览器卡顿,当数据量比较大时甚至浏览器崩溃。
解决方法:加一个 beforeDestroy()方法释放该页面的 chart 资源,我也试过使用 dispose()方法,但是 dispose 销毁这个图例,图例是不存在了,但图例的 resize()方法会启动,则会报没有 resize 这个方法,而 clear()方法则是清空图例数据,不影响图例的 resize,而且能够释放内存,切换的时候就很顺畅了。
beforeDestroy () {
this.chart.clear()
}
ES6中防止内存泄漏
在平时编程中我们可能没有及时的清除引用,ES6中新增了两种数据结构:weakset 和 weakmap,他们对值得引用时不计入垃圾回收机制的,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存。
const wm = new WeakMap()
const element = document.getElementById('example')
vm.set(element, 'something')
vm.get(element)
上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对 element 的引用就是弱引用,不会被计入垃圾回收机制。
注册监听事件的 listener 对象很适合用 WeakMap 来实现
<div id="example">点击</div>
// 代码1
element.addEventListener('click', handler, false)
// 代码2
const listener = new WeakMap()
listener.set(element, handler)
element.addEventListener('click', listener.get(element), false)
function handler(){
console.log('测试')
}
好处是:由于监听函数是放在 WeakMap 里面,一旦 dom 对象 ele 消失,与它绑定的监听函数 handler 也会自动消失。
以上总结有些是平时工作中遇到的需要注意的点,有些是借鉴别人的blog:
前端常见内存泄漏及解决方案_傲娇的koala的博客-CSDN博客
https://mp.weixin.qq.***/s/TEs6JKQsRo2ZbVhVfAuOBA