前端常见内存泄漏及解决方法

进程中不再用到的内存,如果没有及时释放,就叫内存泄漏(memory leak)。这些内存泄漏可能会导致应用程序变得缓慢或不稳定,并且可能会导致浏览器崩溃。

意外的全局变量

1 未声明变量(也就是全局变量)

function f1 () {
  arr = []  // 相当于window.arr = []
}
f1()

2 使用 this 创建的变量(执行函数 this 的指向是 window,实例对象 this 才是指向对象本身)

结合Devtools > Memory 看一下

解决方法:

  1. 避免创建全局变量
  2. 使用严格模式,在 JavaScript 文件头部或者函数的顶部加上 use strict,假如没有声明变量就会提示:Uncaught ReferenceError: xxx is not defined

没有清理的DOM元素引用

var elements = []
function createElements() {
  for(let i=0; i < 100000; i++) {
    let elem = document.createElement("p")
    elem.textContent = 'markbuild'
    elements.push(elem)
    document.body.appendChild(elem)
  }
}
function removeElements() {
    document.querySelectorAll('p').forEach(item => {
      document.body.removeChild(item)
      item.remove()
    })
    // 虽然元素从 DOM 中删除了,但是全局的 elements 对象依然引用了各个结点,所以内存不会被释放
    // 没有手动删除
    // elements = null
}
setTimeout(function() {
  createElements()
},1000)
setTimeout(function() {
  removeElements()
},10000)
未手动删除对DOM 结点的引用:堆内存不被回收 VS 手动删除对DOM 结点的引用:堆内存被回收

闭包引起的内存泄漏

闭包通过保留外部函数的作用域,使内部函数仍然可以访问到外部函数的变量,即使函数外部的变量已经被销毁。

闭包可以避免全局命名污染或冲突,有利于模块化开发,比如 jQuery 就使用了闭包。还可以缓存变量,常见的 counter 计时。

闭包会保留函数执行环境中的变量,这些变量可能会被垃圾收集器误认为仍然在使用中,从而导致内存泄漏。

我们做个实验,代码在图中左侧,然后在Devtools > Memory 中生成堆快照。

通过堆快照,我们可以发现存在的闭包,超级多,大多时内置的,我们就看后面有代码地址(文件和代码行)的。

首先,我们应该避免在全局作用域中创建闭包,可以将闭包变量的作用域限制在局部作用域中,减少内存泄漏的可能。上面counter 因为在局部作用域中,被内存回收,所以在快照中已经找不到,但5个点击事件的闭包回调函数是可以找到的,它没有被回收,鼠标放在上面停顿还会出现下拉框,能看到引用信息,他引用了Block {i: 1},也就是循环体里的变量 i。

当我们在控制台中解绑这 5 个监听,并生成快照2 。最终发现,在快照2中已找不到这5个闭包了,所以按需解绑监听,也能避免内存泄漏。

被遗忘的定时器或回调

var serverData = loadData()
setInterval(function() {
    var renderer = document.getElementById('renderer')
    if(renderer) { 
        renderer.innerHTML = JSON.stringify(serverData)
    } 
}, 5000); 

上面这个例子,需要在完成任务后 clearInterval

var element = document.getElementById('launch-button')
function onClick(event) {
   element.innerHtml = 'xxx'
}
element.addEventListener('click', onClick)

需要手动清除

element.removeEventListener('click', onClick) 
element = null

总之,避免内存泄漏的关键是及时释放不再使用的对象和资源,以便垃圾收集器可以回收它们。

Map 与WeakMap

WeakMap 的 key 必须为引用值,当这个key不被引用,会被GC 回收,从而从WeakMap 里释放。但 Map 的 key 影响 GC 回收, 导致内存泄漏。

Vue 中用 Weakmap 来设计响应系统,用来以被代理的 target 为 key 存储副作用函数,当被代理的 target 对象没有任何引用了,就应该被 GC 自动回收,如果用Map 来存就会导致内存泄露。

const targetMap = new WeakMap()