『前端优化』—— iview中的内存泄露

5,765 阅读3分钟

前言

最近产品经理跟我反馈公司的一个系统频繁切换主菜单时,浏览器占用内存一直在飙升。当时第一反应内存泄露了。 经过场景重现,发现从A主菜单切到B主菜单,在切回到A主菜单,浏览器占用内存翻了一倍,基本可以确认是内存泄露。

一、排查过程

根据BUG场景重现,浏览器占用内存是翻了一倍,可以初步确定是Vue组件没被释放掉。

在Vue中,如果一个Vue组件存在内存泄露,那和该组件相互引用的组件也不能被销毁掉。为什么会这样可以看下面解释?

首先要明白Vue组件最后会被渲染成一个DOM树,比如一个表格组件中是由多个单元格组件构成,如果你在JS代码中保留对一个单元格的引用。在将来你决定将这个表格从DOM中移除,仍旧保留这个单元格的引用。你可能会认为 GC 会回收除了这个单元格之外所有的东西,但是实际上不会。单元格是表格的一个子节点且所有子节点都保留着它们父节点的引用。换句话说,JS代码中对单元格的引用导致整个表格被保留在内存中。

所以用Chrome开发者工具中Memory功能排查,你会发现怎么每个组件都存在内存泄漏,导致无从下手。

这时候我们就要采用排除法来找出泄露点,之前说过是切换主菜单时发生内存泄漏,那首先把每个菜单页面内容全注释,再切换主菜单,经测试没有内存泄漏。

把A主菜单页面中一个个组件打开注释,切换A、B主菜单测试,很幸运发现泄露点,Split 面板分割组件存在内存泄漏。

为了排除其他影响,单独起一个测试项目,引进iview中Split 面板分割组件,切换路由,发现有内存泄漏。

查看项目中iview是3.4.2版本,上GitHub查看源码。

在mounted钩子函数中有个,window绑定resize的监听,但是没有在beforeDestroy钩子函数中解绑,将该组件代码复制到测试项目中,改成

methods:{
    handleUp () {
        this.isMoving = false;
        document.removeEventListener('mousemove',this.handleMove,false);
        document.removeEventListener('mouseup',this.handleUp,false);
        this.$emit('on-move-end');
    },
    handleMousedown (e) {
        this.initOffset = this.isHorizontal ? e.pageX : e.pageY;
        this.oldOffset = this.value;
        this.isMoving = true;
        document.addEventListener('mousemove',this.handleMove,false);
        document.addEventListener('mouseup',this.handleUp,false);
        this.$emit('on-move-start');
    },
    computeOffset(){
        this.offset = (this.valueIsPx ? this.px2percent(this.value, this.$refs.outerWrapper[this.offsetSize]) : this.value) * 10000 / 100;
    }
},
watch: {
    value () {
        this.computeOffset();
    }
},
mounted () {
    this.$nextTick(() => {
        this.computeOffset();
    });
    window.addEventListener('resize',this.computeOffset);
},
beforeDestroy(){
    window.removeEventListener('resize',this.computeOffset);
}

重新测试,发现内存泄漏消失

回到原来项目中,将iview中Split 面板分割组件替换成修改好的组件,经测试,没有发生内存泄漏。

二、关于移除事件监听的坑

在iview中Split 面板分割组件中有这样的代码

mounted () {
    this.$nextTick(() => {
        this.computeOffset();
    });
    window.addEventListener('resize', ()=>{
        this.computeOffset();
    });
}

那在beforeDestroy中要怎么移除这个事件监听

beforeDestroy(){
    window.removeEventListener('resize', ()=>{
        this.computeOffset();
    });
}

或者

beforeDestroy(){
    window.removeEventListener('resize',this.computeOffset)
}

经测试,你会发现内存泄漏还是没解决,你会觉得很奇怪,明明已经移除事件监听,为啥还会内存泄漏,其实这边有个坑在这边,下面就给大家介绍一下。

如果要移除事件监听,必须满足以下几点

  • addEventListener() 的执行函数必须使用外部函数,例:window.addEventListener('resize',this.computeOffset)

  • 执行函数不能是匿名函数,例:window.addEventListener('resize', ()=>{this.computeOffset()})

  • 执行函数不能改变this指向,例window.addEventListener('resize',this.computeOffset.bind(this))

  • addEventListenerremoveEventListener第三个参数必须一致,例:

    window.addEventListener('resize',this.computeOffset,true);
    window.removeEventListener('resize',this.computeOffset,true);
    
    window.addEventListener('resize',this.computeOffset,false);
    window.removeEventListener('resize',this.computeOffset,false);