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 相关文章推荐
js的with语句使用方法
Sep 21 Javascript
js Form.elements[i]的使用实例
Nov 13 Javascript
node爬取微博的数据的简单封装库nodeweibo使用指南
Jan 02 Javascript
js获取html的span标签的值方法(超简单)
Jul 26 Javascript
Vuejs入门教程之Vue生命周期,数据,手动挂载,指令,过滤器
Apr 19 Javascript
jQuery实现简单日期格式化功能示例
Sep 19 jQuery
Js判断H5上下滑动方向及滑动到顶部和底部判断的示例代码
Nov 15 Javascript
element ui table 增加筛选的方法示例
Nov 02 Javascript
layui.use模块外部使用其内部定义的js封装函数方法
Sep 16 Javascript
微信小程序实现侧边栏分类
Oct 21 Javascript
微信小程序实现音乐播放器
Nov 20 Javascript
ES6中new Function()语法及应用实例分析
Feb 19 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
用 Composer构建自己的 PHP 框架之使用 ORM
2014/10/30 PHP
利用PHP函数计算中英文字符串长度的方法
2014/11/11 PHP
php获取从html表单传递数组的方法
2015/03/20 PHP
JScript中使用ADODB.Stream判断文件编码的代码
2008/06/09 Javascript
Javascript typeof 用法
2008/12/28 Javascript
一个简单的javascript类定义例子
2009/09/12 Javascript
javascript 面向对象全新理练之继承与多态
2009/12/03 Javascript
js的逻辑运算符 ||
2010/05/31 Javascript
js数组的操作详解
2013/03/27 Javascript
javascript强大的日期函数代码分享
2013/09/04 Javascript
常用的Javascript数据验证插件
2015/08/04 Javascript
JS实现alert中显示换行的方法
2015/12/17 Javascript
JS中mouseover和mouseout多次触发问题如何解决
2016/06/06 Javascript
详解angular2封装material2对话框组件
2017/03/03 Javascript
vue项目打包后怎样优雅的解决跨域
2019/05/26 Javascript
JS根据Unix时间戳显示发布时间是多久前【项目实测】
2019/07/10 Javascript
微信小程序点击顶部导航栏切换样式代码实例
2019/11/12 Javascript
Vue循环中多个input绑定指定v-model实例
2020/08/31 Javascript
DWR内存兼容及无法调用问题解决方案
2020/10/16 Javascript
Python生成pdf文件的方法
2014/08/04 Python
python爬取51job中hr的邮箱
2016/05/14 Python
python pandas dataframe 按列或者按行合并的方法
2018/04/12 Python
python读取和保存图片5种方法对比
2018/09/12 Python
Python使用POP3和SMTP协议收发邮件的示例代码
2019/04/16 Python
python实现批量修改服务器密码的方法
2019/08/13 Python
Python requests.post方法中data与json参数区别详解
2020/04/30 Python
Python 使用生成器代替线程的方法
2020/08/04 Python
python可视化分析的实现(matplotlib、seaborn、ggplot2)
2021/02/03 Python
利用CSS3实现文本框的清除按钮相关的一些效果
2015/06/23 HTML / CSS
LA MER海蓝之谜美国官网:传奇面霜
2016/08/27 全球购物
罗技英国官方网站:Logitech UK
2020/11/03 全球购物
c/c++某大公司的两道笔试题
2014/02/02 面试题
教师个人的自我评价分享
2014/01/02 职场文书
追悼会上的答谢词
2014/01/10 职场文书
《挑山工》的教学反思
2014/02/16 职场文书
MySQL Router的安装部署
2021/04/24 MySQL