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 相关文章推荐
常用参考资料(手册)下载或者链接
Jul 22 Javascript
IE8 下的Js错误HTML Parsing Error...
Aug 14 Javascript
网页禁用右键实现代码(JavaScript代码)
Oct 29 Javascript
javascript闭包入门示例
Apr 30 Javascript
JavaScript实现的in_array函数
Aug 27 Javascript
jquery判断当前浏览器的实现代码
Nov 07 Javascript
jQuery实现简单的Ajax调用功能示例
Feb 15 jQuery
关于vue状态过渡transition不起作用的原因解决
Apr 09 Javascript
JS操作字符串转数字的常见方法示例
Oct 29 Javascript
jstree中的checkbox默认选中和隐藏示例代码
Dec 29 Javascript
vue 组件销毁并重置的实现
Jan 13 Javascript
原生javascript如何实现共享onload事件
Jul 03 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
WINDOWS 2000下使用ISAPI方式安装PHP
2006/09/05 PHP
php开发工具之vs2005图解
2008/01/12 PHP
golang与PHP输出excel示例
2016/07/22 PHP
php5.5使用PHPMailer-5.2发送邮件的完整步骤
2018/10/14 PHP
TP5框架model常见操作示例小结【增删改查、聚合、时间戳、软删除等】
2020/04/05 PHP
PJBlog插件 防刷新的在线播放器
2006/10/25 Javascript
IE6中使用position导致页面变形的解决方案(js代码)
2011/01/09 Javascript
避免回车键导致的页面无意义刷新的解决方法
2011/04/12 Javascript
21个值得收藏的Javascript技巧
2014/02/04 Javascript
分享9个最好用的JavaScript开发工具和代码编辑器
2015/03/24 Javascript
JavaScript节点及列表操作实例小结
2015/08/05 Javascript
JavaScript中的this关键字使用详解
2015/08/14 Javascript
JavaScript位置与大小(1)之正确理解和运用与尺寸大小相关的DOM属性
2015/12/26 Javascript
javascript的正则匹配方法学习
2016/02/24 Javascript
微信小程序商城项目之商品属性分类(4)
2017/04/17 Javascript
使用Bootstrap4 + Vue2实现分页查询的示例代码
2017/12/21 Javascript
微信小程序支付功能 php后台对接完整代码分享
2018/06/12 Javascript
使用vue实现HTML页面生成图片的方法
2020/03/12 Javascript
使用TS来编写express服务器的方法步骤
2020/10/29 Javascript
[04:52]DOTA2亚洲邀请赛附加赛 TOP10精彩集锦
2015/01/29 DOTA
关于python2 csv写入空白行的问题
2018/06/22 Python
python安装twisted的问题解析
2018/08/21 Python
Python代码打开本地.mp4格式文件的方法
2019/01/03 Python
扩展Django admin的list_filter()可使用范围方法
2019/08/21 Python
Python通过Manager方式实现多个无关联进程共享数据的实现
2019/11/07 Python
在 Pycharm 安装使用black的方法详解
2020/04/02 Python
Python使用tkinter实现摇骰子小游戏功能的代码
2020/07/02 Python
keras分类模型中的输入数据与标签的维度实例
2020/07/03 Python
史上最详细的Python打包成exe文件教程
2021/01/17 Python
澳大利亚珍珠首饰购物网站:Vayo Pearls
2019/03/11 全球购物
工伤死亡理赔协议书
2014/10/20 职场文书
优秀班主任材料
2014/12/16 职场文书
文员岗位职责
2015/02/04 职场文书
2015年政教主任工作总结
2015/07/23 职场文书
2016民族团结先进个人事迹材料
2016/02/26 职场文书
CSS 实现多彩、智能的阴影效果
2021/05/12 HTML / CSS