vue使用中的内存泄漏【推荐】


Posted in Javascript onJuly 10, 2018

今天看到一篇关于js使用中内存泄露的文章,以及chrom浏览器查看内存泄漏的方法,决定留着。本文只截取了我认为比较重要的部分,喜欢原文的小伙伴,请点击文章下方的原文链接。

什么是内存泄露?内存泄露是指new了一块内存,但无法被释放或者被垃圾回收。new了一个对象之后,它申请占用了一块堆内存,当把这个对象指针置为null时或者离开作用域导致被销毁,那么这块内存没有人引用它了在JS里面就会被自动垃圾回收。但是如果这个对象指针没有被置为null,且代码里面没办法再获取到这个对象指针了,就会导致无法释放掉它指向的内存,也就是说发生了内存泄露。为什么代码里面会拿不到这个对象指针了呢,举一个例子:

// module date.js
let date = null;
export default {
 init () {
  date = new Date();
 }
}
// main.js
import date from 'date.js';
date.init();

在main.js初始化了date之后,date这个变量就一会直存在了,直到你把页面关了,因为date的引用是在另一个module里面,可以理解为模块就是一个闭包对外是不可见的。所以如果你是希望这个date对象一直存在、需要一直使用的话,那么没有问题,但是如果想用一次就不用了那就会有问题,这个对象一直在内存里面没有被释放就发生了内存泄露。

另一种比较隐蔽并且很常见的内存泄露是事件绑定,形成了一个闭包,导致一些变量一直存在。如下例子所示:

// 一个图片懒惰加载引擎示例
class ImageLazyLoader {
 constructor ($photoList) {
  $(window).on('scroll', () => {
   this.showImage($photoList);
  });
 }
 showImage ($photoList) {
  $photoList.each(img => {
   // 通过位置判断图片滑出来了就加载
   img.src = $(img).attr('data-src');
  });
 }
}
// 点击分页的时候就初始化一个图片懒惰加载的
$('.page').on('click', function () {
 new ImageLazyLoader($('img.photo'));
});

这是一个图片懒惰加载的模型,每次点分页的时候就会清掉上一页的数据更新为当前页的DOM,并重新初始化一个懒惰加载的引擎。它里面监听了scroll事件,对传进来的图片列表的DOM进行处理。每点一次分页就会重新new一个,这里就发生了内存泄露,主要是以下3行代码导致的:

$(window).on('scroll', () => {
 this.showImage($photoList);
});

因为这里的事件绑定形成了一个闭包,this/$photoList这两个变量一直没有被释放,this是指向ImageLazyLoader的实例,而$photoList是指向DOM结点,当清除掉上一页的数据的时候,相关DOM结点已经从DOM树分离出来了,但是仍然还有一个$photoList指向它们,导致这些DOM结点无法被垃圾回收一直在内存里面,就发生了内存泄露。由于this变量也被闭包困住了没有被释放,所以还有一个ImageLazyLoader的实例发生内存泄露。

这个的解决方法比较简单,就是销毁实例的时候把绑定的事件off掉,如下代码所示:

class ImageLazyLoader {
 constructor ($photoList) {
  this.scrollShow = () => {
   this.showImage($photoList);
  };
  $(window).on('scroll', this.scrollShow);
 }
 // 新增一个事件解绑       
 clear () {      
  $(window).off('scroll', this.scrollShow);
 }
 showImage ($photoList) {
  $photoList.each(img => {
   // 通过位置判断图片滑出来了就加载
   img.src = $(img).attr('data-src');
  });
  // 判断如果图片已全部显示,就把事件解绑了
  if (this.allShown) {
   this.clear();
  }
 }
}
// 点击分页的时候就初始化一个图片懒惰加载的
let lazyLoader = null;
$('.page').on('click', function () {
 lazyLoader && (lazyLoader.clear());
 lazyLoader = new ImageLazyLoader($('img.photo'));
});

在每次实例化一个ImageLazyLoader之前把先把上一个实例clear掉,clear里面进行解绑,由于JS有构造函数但是没有解构函数,所以需要自己写一个clear,在外面手动调一下clear。同时在事件的执行过程的合适时机自动把事件给解绑了,上面是判断如果所有的图片都展示出来了那么就没必要监听scroll事件了直接解绑了。这样就能解决内存泄露的问题了,能够触发自动垃圾回收。

为什么把事件解绑了,就不会有闭包引用了呢?因为JS引擎检测到那个闭包没用了,就把那个闭包销毁了,那么闭包引用的外部变量也自然会被置空。

好了,基础知识就讲解到这里,现在用Chrome devtools的内存检测工具来实际操作一遍,方便发现页面的一些内存泄露行为。为了避免装给浏览器装的一些插件造成影响,使用Chome的隐身模式页面,它会把所有的插件都给禁掉。

然后打开devtools,切到Memory的tab,选中Heap snapshot,如下所示:

vue使用中的内存泄漏【推荐】 

什么叫heap snapshot呢?翻译一下就是堆快照,给当前内存堆拍一张照片。因为动态申请的内存都是在堆里面的,而局部变量是在内存栈里面,是由操作系统分配管理的是不会内存泄露了。所以关心堆的情况就好了。

然后做一些增删改DOM的操作,如:

(1)弹一个框,然后把弹框给关了

(2)单页面的点击跳转到另一个路由,然后再点后退返回

(3)点击分页触发动态改DOM

就是先增加DOM,然后把这些DOM给删了,看一下这些被删除的DOM是否还有对象引用它们。

这里我是第2种方式的场景,检测单页面应用的某个路由页面是否存在内存泄露。先打开首页,点到另一个页面,再点后退,接着点一下垃圾回收的按钮:

vue使用中的内存泄漏【推荐】 

触发垃圾回收,避免一些不必要的干扰。

然后再点一下拍照按钮:

vue使用中的内存泄漏【推荐】 

它就会把当前页面的内存堆扫描一遍显示出来,如下图所示:

vue使用中的内存泄漏【推荐】 

然后在上面中间的Class Filter的搜索框里搜一下detached:

vue使用中的内存泄漏【推荐】 

它就会显示所有已经分离了DOM树的DOM结点,重点关注distance值不为空的,这个distance表示距离DOM根结点的距离。上图展示的这些div具体是啥呢?我们把鼠标放上去不动等个2s,它就会显示这个div的DOM信息:

vue使用中的内存泄漏【推荐】 

通过className等信息可以知道它就是那个要检查的页面的DOM节点,在下面的Object的窗口里面依次展开它的父结点,可以看到它最外面的父结点是一个VueComponent实例:

vue使用中的内存泄漏【推荐】 

下面黄色字体native_bind表示有个事件指向了它,黄色表示引用仍然生效,把鼠标放到native_bind上面停留2秒:

它会提示你是在homework-web.vue这个文件有一个getScale函数绑定在了window上面,查看一下这个文件确实是有一个绑定:

mounted () {
 window.addEventListener('resize', this.getScale);
}

所以虽然Vue组件把DOM删除了,但是还有个引用存在,导致组件实例没有被释放,组件里面又有一个$el指向DOM,所以DOM也没有被释放。

要在beforeDestroyed里面解绑的

beforeDestroyed () {
 window.removeEventListener('resize', this.getScale);
}

所以综合上面的分析,造成内存泄露的可能会有以下几种情况:

(1)监听在window/body等事件没有解绑

(2)绑在EventBus的事件没有解绑

(3)Vuex的$store watch了之后没有unwatch

(4)模块形成的闭包内部变量使用完后没有置成null

(5)使用第三方库创建,没有调用正确的销毁函数

并且可以借助Chrome的内存分析工具进行快速排查,本文主要是用到了内存堆快照的基本功能,读者可以尝试分析自己的页面是否存在内存泄漏,方法是做一些操作如弹个框然后关了,拍一张堆快照,搜索detached,按distance排序,把非空的节点展开父级,找到标黄的字样说明,那些就是存在没有释放的引用。也就是说这个方法主要是分析仍然存在引用的游离DOM节点。因为页面的内存泄露通常是和DOM相关的,普通的JS变量由于有垃圾回收所以一般不会有问题,除非使用闭包把变量困住了用完了又没有置空。

DOM相关的内存泄露通常也是因为闭包和事件绑定引起的。绑了(全局)事件之后,在不需要的时候需要把它解绑。当然直接绑在div上面的可以直接把div删了,绑在它上面的事件就自然解绑了。

Javascript 相关文章推荐
List the UTC Time on a Computer
Jun 11 Javascript
javascript解三阶幻方(九宫格)
Apr 22 Javascript
jQuery插件Zclip实现完美兼容个浏览器点击复制内容到剪贴板
Apr 30 Javascript
JQuery中Ajax的操作完整例子
Mar 07 Javascript
jquery获取链接地址和跳转详解(推荐)
Aug 15 jQuery
Three.js如何实现雾化效果示例代码
Sep 27 Javascript
Vue登录注册并保持登录状态的方法
Aug 17 Javascript
react脚手架如何配置less和ant按需加载的方法步骤
Nov 28 Javascript
Electron autoUpdater实现Windows安装包自动更新的方法
Dec 24 Javascript
Vue项目从webpack3.x升级webpack4不完全指南
Apr 28 Javascript
Vue如何获取数据列表展示
Dec 11 Javascript
微信小程序webSocket的使用方法
Feb 20 Javascript
Vue脚手架的简单使用实例
Jul 10 #Javascript
vue自定义移动端touch事件之点击、滑动、长按事件
Jul 10 #Javascript
微信小程序中换行空格(多个空格)写法详解
Jul 10 #Javascript
在小程序中使用腾讯视频插件播放教程视频的方法
Jul 10 #Javascript
Angular5中提取公共组件之radio list的实例代码
Jul 10 #Javascript
ng-alain表单使用方式详解
Jul 10 #Javascript
JavaScript基于对象方法实现数组去重及排序操作示例
Jul 10 #Javascript
You might like
Syphon 使用方法
2021/03/03 冲泡冲煮
使用PHP Socket写的POP3类
2013/10/30 PHP
自编函数解决pathinfo()函数处理中文问题
2014/11/03 PHP
php常用字符串处理函数实例分析
2014/11/22 PHP
php中in_array函数用法探究
2014/11/25 PHP
php文件下载处理方法分析
2015/04/22 PHP
教你在PHPStorm中配置Xdebug
2015/07/27 PHP
Smarty模板变量与调节器实例详解
2019/07/20 PHP
PHP优化之批量操作MySQL实例分析
2020/04/23 PHP
jquery 简短右键菜单 多浏览器兼容
2010/01/01 Javascript
js 中 document.createEvent的用法
2010/08/29 Javascript
JavaScript作用域链示例分享
2014/05/27 Javascript
jQuery检测鼠标左键和右键点击的方法
2015/03/17 Javascript
九种原生js动画效果
2015/11/11 Javascript
React Native自定义控件底部抽屉菜单的示例
2018/02/08 Javascript
vue中element-ui表格缩略图悬浮放大功能的实例代码
2018/06/26 Javascript
React 使用recharts实现散点地图的示例代码
2018/12/07 Javascript
JSONP解决JS跨域问题的实现
2020/05/25 Javascript
vue 路由缓存 路由嵌套 路由守卫 监听物理返回操作
2020/08/06 Javascript
javascript贪吃蛇游戏设计与实现
2020/09/17 Javascript
addEventListener()和removeEventListener()追加事件和删除追加事件
2020/12/04 Javascript
[02:05]2014DOTA2国际邀请赛 BBC外卡赛赛后总结
2014/07/09 DOTA
python中urlparse模块介绍与使用示例
2017/11/19 Python
关于django 数据库迁移(migrate)应该知道的一些事
2018/05/27 Python
Python中xml和json格式相互转换操作示例
2018/12/05 Python
django之对FileField字段的upload_to的设定方法
2019/07/28 Python
Python3 合并二叉树的实现
2019/09/30 Python
pandas to_excel 添加颜色操作
2020/07/14 Python
Python 操作 MySQL数据库
2020/09/18 Python
Amaze UI 文件选择域的示例代码
2020/08/26 HTML / CSS
大专生自我鉴定怎么写
2014/09/16 职场文书
县委务虚会发言材料
2014/10/20 职场文书
2014个人年终工作总结范文
2014/12/15 职场文书
公务员年度考核登记表个人总结
2015/02/12 职场文书
详解JS WebSocket断开原因和心跳机制
2021/05/07 Javascript
python中的sys模块和os模块
2022/03/20 Python