vue使用中的内存泄漏

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

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

// 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对象一直存在、须要一直使用的话,那么没有问题,可是若是想用一次就不用了那就会有问题,这个对象一直在内存里面没有被释放就发生了内存泄露。浏览器

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

// 一个图片懒惰加载引擎示例
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引擎检测到那个闭包没用了,就把那个闭包销毁了,那么闭包引用的外部变量也天然会被置空。ui

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

而后打开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删了,绑在它上面的事件就天然解绑了。

原文地址