vue使用中的内存泄漏【推荐】
今天看到一篇关于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,如下所示:
什么叫heap snapshot呢?翻译一下就是堆快照,给当前内存堆拍一张照片。因为动态申请的内存都是在堆里面的,而局部变量是在内存栈里面,是由操作系统分配管理的是不会内存泄露了。所以关心堆的情况就好了。
然后做一些增删改dom的操作,如:
(1)弹一个框,然后把弹框给关了
(2)单页面的点击跳转到另一个路由,然后再点后退返回
(3)点击分页触发动态改dom
就是先增加dom,然后把这些dom给删了,看一下这些被删除的dom是否还有对象引用它们。
这里我是第2种方式的场景,检测单页面应用的某个路由页面是否存在内存泄露。先打开首页,点到另一个页面,再点后退,接着点一下垃圾回收的按钮:
触发垃圾回收,避免一些不必要的干扰。
然后再点一下拍照按钮:
它就会把当前页面的内存堆扫描一遍显示出来,如下图所示:
然后在上面中间的class filter的搜索框里搜一下detached:
它就会显示所有已经分离了dom树的dom结点,重点关注distance值不为空的,这个distance表示距离dom根结点的距离。上图展示的这些div具体是啥呢?我们把鼠标放上去不动等个2s,它就会显示这个div的dom信息:
通过classname等信息可以知道它就是那个要检查的页面的dom节点,在下面的object的窗口里面依次展开它的父结点,可以看到它最外面的父结点是一个vuecomponent实例:
下面黄色字体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删了,绑在它上面的事件就自然解绑了。
上一篇: 凡仙情缘