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 相关文章推荐
jquery获取自定义属性(attr和prop)实例介绍
Apr 21 Javascript
Node.js中使用事件发射器模式实现事件绑定详解
Aug 15 Javascript
JS获取图片lowsrc属性的方法
Apr 01 Javascript
js贪吃蛇网页版游戏特效代码分享(挑战十关)
Aug 24 Javascript
JavaScript对象参数的引用传递
Jan 14 Javascript
探讨JavaScript语句的执行过程
Jan 28 Javascript
浅析创建javascript对象的方法
May 13 Javascript
JS字符串按逗号和回车分隔的方法
Apr 25 Javascript
在vue里面设置全局变量或数据的方法
Mar 09 Javascript
webstorm和.vue中es6语法报错的解决方法
May 08 Javascript
JavaScript函数定义方法实例详解
Mar 05 Javascript
js实现带搜索功能的下拉框
Jan 11 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
用ODBC的分页显示
2006/10/09 PHP
php str_pad 函数用法简介
2009/07/11 PHP
php学习笔记(三)操作符与控制结构
2011/08/06 PHP
php之XML转数组函数的详解
2013/06/07 PHP
php结合ACCESS的跨库查询功能
2015/06/12 PHP
PHP实现时间比较和时间差计算的方法示例
2017/07/24 PHP
ThinkPHP5框架缓存查询操作分析
2018/05/30 PHP
执行iframe中的javascript方法
2008/10/07 Javascript
javascript 动态数据下的锚点错位问题解决方法
2008/12/24 Javascript
javascript 数据类型转换(parseInt,parseFloat)
2010/07/20 Javascript
Javascript 面向对象(二)封装代码
2012/05/23 Javascript
JavaScript中Null与Undefined的区别解析
2015/06/30 Javascript
微信小程序使用第三方库Immutable.js实例详解
2016/09/27 Javascript
jQuery继承extend用法详解
2016/10/10 Javascript
15个非常实用的JavaScript代码片段
2016/12/18 Javascript
angular6 填坑之sdk的方法
2018/12/27 Javascript
如何解决js函数防抖、节流出现的问题
2019/06/17 Javascript
[08:08]2014DOTA2国际邀请赛中国区预选赛精彩TOPPLAY
2014/06/25 DOTA
python  创建一个保留重复值的列表的补码
2018/10/15 Python
python如何给字典的键对应的值为字典项的字典赋值
2019/07/05 Python
python cv2读取rtsp实时码流按时生成连续视频文件方式
2019/12/25 Python
pycharm内无法import已安装的模块问题解决
2020/02/12 Python
解决Python import docx出错DLL load failed的问题
2020/02/13 Python
python3发送request请求及查看返回结果实例
2020/04/30 Python
PyQt5.6+pycharm配置以及pyinstaller生成exe(小白教程)
2020/06/02 Python
python获取时间戳的实现示例(10位和13位)
2020/09/23 Python
pycharm中选中一个单词替换所有重复单词的实现方法
2020/11/17 Python
css3的focus-within选择器的使用
2020/05/11 HTML / CSS
微软美国官方网站:Microsoft美国
2018/05/10 全球购物
美国电子产品主要品牌的授权在线零售商:DataVision
2019/03/23 全球购物
Python面试题:如何用Python来发送邮件
2016/03/15 面试题
汽车运用工程专业毕业生推荐信
2013/12/25 职场文书
小学英语教师先进事迹
2014/05/28 职场文书
毕业生工作求职信
2014/06/30 职场文书
美术第二课堂活动总结
2014/07/08 职场文书
项目战略合作意向书
2015/05/08 职场文书