进程中不再用到的内存,如果没有及时释放,就叫内存泄漏(memory leak)。这些内存泄漏可能会导致应用程序变得缓慢或不稳定,并且可能会导致浏览器崩溃。
意外的全局变量
1 未声明变量(也就是全局变量)
function f1 () {
arr = [] // 相当于window.arr = []
}
f1()
2 使用 this 创建的变量(执行函数 this 的指向是 window,实例对象 this 才是指向对象本身)
解决方法:
- 避免创建全局变量
- 使用严格模式,在 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)
闭包引起的内存泄漏
闭包通过保留外部函数的作用域,使内部函数仍然可以访问到外部函数的变量,即使函数外部的变量已经被销毁。
闭包可以避免全局命名污染或冲突,有利于模块化开发,比如 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
Vue 中用 Weakmap 来设计响应系统,用来以被代理的 target 为 key 存储副作用函数,当被代理的 target 对象没有任何引用了,就应该被 GC 自动回收,如果用Map 来存就会导致内存泄露。
const targetMap = new WeakMap()