其他
Vue系列之常见内存泄漏定位与解决
作者简介:李红,学而思一对一前端专家,十多年一线研发经验,专注前端微服务、组件开发、浏览器兼容性处理、性能优化。
技术专长: Vue,React,TypeScript,Node
导读:JavaScript 有完善的内存处理机制,能自动进行垃圾回收,但是假如一个对象一直被引用,他的内存是无法得到释放的。如果项目运行过程中,内存占用越来越高,只增不减,没有峰值,就存在内存泄漏。多页应用我们可以通过页面刷新缓解,但是对于服务端渲染和单页应用则需要重点关注内存泄漏问题。本文主要以Vue单页应用展开,因为在 SPA 的设计中,用户使用它时是不需要刷新浏览器的,所以 JavaScript 应用需要自行清理组件来确保垃圾回收以预期的方式生效。
01
什么是内存泄漏
02
如何判断内存泄漏
Heap snapshot - 用以打印堆快照,堆快照文件显示页面的 javascript 对象和相关 DOM 节点之间的内存分配 Allocation instrumentation on timeline - 在时间轴上记录内存信息,随着时间变化记录内存信息。 Allocation sampling - 内存信息采样,使用采样的方法记录内存分配。此配置文件类型具有最小的性能开销,可用于长时间运行的操作。它提供了由 javascript 执行堆栈细分的良好近似值分配。
03
如何定位内存泄漏
04
常见的内存泄漏
意外的全局变量 函数中意外的定义了全局变量,每次执行该函数都会生成该变量,且不会随着函数执行结束而释放; 未清除的定时器 定时器没有清除,它内部引用的变量,不会被释放; 脱离DOM的元素引用
一个dom容器删除之后,变量未置为null,则其内部的dom元素则不会释放;持续绑定的事件
函数中addEventListener绑定事件,函数多次执行,绑定便会产生多次,产生内存泄漏;绑在EventBus的事件没有解绑; 闭包引起内存泄漏
比如事件处理回调,导致DOM对象和脚本中对象双向引用;使用第三方库创建,没有调用正确的销毁函数; 单页应用时,页面路由切换后,内存未释放。
05
解决内存泄漏
变量先申明后使用; setTimeout setInterval清理 (最好不用)可以使用nextTick代替; 如果在mounted/created 钩子中绑定了DOM/BOM对象中的事件,需要在beforeDestroy中做对应解绑处理。 mounted () {
window.addEventListener('resize', this.onResize)
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize)
}
如果在mounted/created 钩子中使用了on,需要在beforeDestroy 中做对应解绑(off)处理。 mounted () {
this.$EventBus.$on('exitClassRoom',this.exitClassRoomHandle)
},
destroyed () {
this.$EventBus.$off('exitClassRoom',this.exitClassRoomHandle)
}
如果在mounted/created 钩子中使用了第三方库初始化,需要在beforeDestroy 中做对应销毁处理。 慎用keep-alive
当你用 keep-alive 包裹一个组件后,它的状态就会保留,因此就留在了内存里,切莫在整个路由页面上加上keep-alive。
一旦你使用了 keep-alive,那么你就可以访问另外两个生命周期钩子:activated和 deactivated。你需要在一个 keep-alive 组件被移除的时候,调用 deactivated 钩子进行清理或改变数据。
06
实际案例
<keep-alive>
<el-container class="teacher-content" v-if="
['teacherWorkbenchCourse', 'teacherWorkbenchReschedule'].indexOf(
this.$router.currentRoute.name
) === -1
" key="teacher-content">
<el-header class="body-banner">
<Banner>
<DhProductLineSelect v-if="this.$router.currentRoute.name == 'teacherWorkbench'"></DhProductLineSelect>
<template v-if="
this.$router.currentRoute.name == 'personMaterial' ||
this.$router.currentRoute.name == 'contentCloud'
">
<BreadCrumbMaterial></BreadCrumbMaterial>
</template>
<template v-if="
this.$router.currentRoute.name == 'searchResult' || this.lessonIsEdit">
<span class="router-go-back">
<img src />
</span>
</template>
<template v-if="!this.lessonIsEdit">
<BreadCrumbs />
</template>
</Banner>
</el-header>
<el-main class="body-main">
<router-view></router-view>
</el-main>
</el-container>
<router-view v-else> </router-view>
</keep-alive>
优化后: <el-container
class="teacher-content"
key="teacher-content"
>
<el-header class="body-banner" :class="[headClass, marginLeft]">
<Banner>
<DhProductLineSelect v-if="this.$router.currentRoute.name == 'teacherWorkbench'||this.$router.currentRoute.name=='teacherWorkbenchCourse'"></DhProductLineSelect>
<template
v-if="
this.$router.currentRoute.name == 'personMaterial' ||
this.$router.currentRoute.name == 'contentCloud'
"
>
<BreadCrumbMaterial></BreadCrumbMaterial>
</template>
<template
v-if="
this.$router.currentRoute.name == 'searchResult' || this.lessonIsEdit"
>
<span class="router-go-back">
<img src />
</span>
</template>
<template v-if="!this.lessonIsEdit">
<BreadCrumbs/>
</template>
</Banner>
</el-header>
<el-main :class="['body-main',mainNoPadding]">
<router-view></router-view>
</el-main>
</el-container>
综上操作,内存耗用截图如下,无用内存大部分得到释放:我知道你“在看”哟~