作者: 文章来源:
本文原地址:https://www.mimiwuqi.com/webqianduan/196798.html
目录文章目录想来很多同学看到内存泄漏,内心直接会跳出两个字:闭包!!!再让你说点其它的估计就噤声了。如果你对内存泄漏的了解仅限于闭包,那真的是应该仔细看此文了,闭包可能会造成内存泄漏,但是内存泄漏并不是只有闭包,它只是内存泄漏的引子之一罢了。
写的程序运行一段时间后慢慢变卡甚至要崩溃了?
此文我们会介绍内存泄漏的相关概念和引起内存泄漏的一些问题,还会着重给大家介绍内存泄漏的排查、定位及修复方法(学到即可用到),最后还简单扩展了下前端内存三大件的其他两件内存膨胀和频繁 GC 的概念。
引擎中有垃圾回收机制,它主要针对一些程序中不再使用的对象,对其清理回收释放掉内存。
那么垃圾回收机制会把不再使用的对象(垃圾)全都回收掉吗?
其实引擎虽然针对垃圾回收做了各种优化从而尽可能的确保垃圾得以回收,但并不是说我们就可以完全不用关心这块了,我们代码中依然要主动避免一些不利于引擎做垃圾回收操作,因为不是所有无用对象内存都可以被回收的,那当不再用到的对象内存,没有及时被回收时,我们叫它 内存泄漏(Memory leak)。
代码不规范,同事两行泪,接下来我们看看会引起内存泄漏的一些常见案例。
闭包就是函数内部嵌套并 return 一个函数???这是大多数人认为的闭包,好吧,它确实也是,我们来看看几本 JS 高光书中的描述:
按照上面三本书中的描述,那闭包所涉及的的范围就比较广了,我们这里暂时不去纠结闭包的定义,就以最简单、大家都认可的闭包例子来看闭包:
function fn1(){
let test = 'isboyjc'
return function(){
console.log('hahaha')
}
}
let fn1Child = fn1()
fn1Child()
上例是闭包吗?它造成内存泄漏了吗?
显然它是一个典型闭包,但是它并没有造成内存泄漏,因为返回的函数中并没有对 fn1 函数内部的引用,也就是说,函数 fn1 内部的 test 变量完全是可以被回收的,那我们再来看:
function fn2(){
let test = new Array(1000).fill('isboyjc')
return function(){
console.log(test)
return test
}
}
let fn2Child = fn2
fn2Child()
上例是闭包吗?它造成内存泄漏了吗?
显然它也是闭包,并且因为 return 的函数中存在函数 fn2 中的 test 变量引用,所以 test 并不会被回收,也就造成了内存泄漏。
那么怎样解决呢?
其实在函数调用后,把外部的引用关系置空就好了,如下:
function fn2(){
let test = new Array(1000).fill('isboyjc')
return function(){
console.log(test)
return test
}
}
let fn2Child = fn2
fn2Child()
fn2Child = null
“ 减少使用闭包,闭包会造成内存泄漏。。。 ”
醒醒,这句话是过去式了,它的描述不准确,So,应该说不正当的使用闭包可能会造成内存泄漏。
我们知道 JavaScript 的垃圾回收是自动执行的,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。
再来看全局变量和局部变量,函数中的局部变量在函数执行结束后这些变量已经不再被需要,所以垃圾回收器会识别并释放它们。但是对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以全局变量通常不会被回收,我们使用全局变量是 OK 的,但同时我们要避免一些额外的全局变量产生,如下:
function fn(){
// 没有声明从而制造了隐式全局变量 test1
test1 = 'isboyjc1'
// 函数内部 this 指向 window,制造了隐式全局变量 test2
this.test2 = 'isboyjc2'
}
fn()
调用函数 fn ,因为 没有声明 和 函数中 this 的问题造成了两个额外的隐式全局变量,这两个变量不会被回收,这种情况我们要尽可能的避免,在开发中我们可以使用严格模式或者通过 lint 检查来避免这些情况的发生,从而降低内存成本。
除此之外,我们在程序中也会不可避免的使用全局变量,这些全局变量除非被取消或者重新分配之外也是无法回收的,这也就需要我们额外的关注,也就是说当我们在使用全局变量存储数据时,要确保使用后将其置空或者重新分配,当然也很简单,在使用完将其置为 null 即可,特别是在使用全局变量做持续存储大量数据的缓存时,我们一定要记得设置存储上限并及时清理,不然的话数据量越来越大,内存压力也会随之增高。
var test = new Array(10000) // do something test = null
考虑到性能或代码简洁方面,我们代码中进行 DOM 时会使用变量缓存 DOM 节点的引用,但移除节点的时候,我们应该同步释放缓存的引用,否则游离的子树无法释放。
<div id="root">
<ul id="ul">
<li></li>
<li></li>
<li id="li3"></li>
<li></li>
</ul>
</div>
<script>
let root = document.querySelector('#root')
let ul = document.querySelector('#ul')
let li3 = document.querySelector('#li3')
// 由于 ul 变量存在,整个 ul 及其子元素都不能 GC
root.removeChild(ul)
// 虽置空了 ul 变量,但由于 li3 变量引用 ul 的子节点,所以 ul 元素依然不能被 GC
ul = null
// 已无变量引用,此时可以 GC
li3 = null
</script>
如上所示,当我们使用变量缓存 DOM 节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏。
假如我们将父节点置空,但是被删除的父节点其子节点引用也缓存在变量里,那么就会导致整个父 DOM 节点树下整个游离节点树均无法清理,还是会出现内存泄漏,解决办法就是将引用子节点的变量也置空,如下图:

分析定位成功,进入下一步骤,修复并再次验证。
由于这是临时写的一个案例,没有具体的场景,所以也就没有办法使用针对性的方式来修复,So,此步骤暂时忽略,不过在项目中我们还是要解决的。
比如全局对象一直增大这个问题,全局对象我们无法避免,但是可以限制一下全局对象的大小,根据场景可以超出就清理一部分。
比如闭包引用的问题,不让它引用,或者执行完置空,这都是上面说过的。
总之,一切都需要根据具体场景选择解决方案,解决之后重复上面排查流程看内存即可。
其实前端关于内存方面主要有三个问题,我把它们亲切的称作内存三大件:
内存泄漏 我们说很久了,对象已经不再使用但没有被回收,内存没有被释放,即内存泄漏,那想要避免就避免让无用数据还存在引用关系,也就是多注意我们上面说的常见的几种内存泄漏的情况。
内存膨胀 即在短时间内内存占用极速上升到达一个峰值,想要避免需要使用技术手段减少对内存的占用。
频繁 GC 同这个名字,就是 GC 执行的特别频繁,一般出现在频繁使用大的临时变量导致新生代空间被装满的速度极快,而每次新生代装满时就会触发 GC,频繁 GC 同样会导致页面卡顿,想要避免的话就不要搞太多的临时变量,因为临时变量不用了就会被回收,这和我们内存泄漏中说避免使用全局变量冲突,其实,只要把握好其中的度,不太过分就 OK。
如果你的程序运行出现卡顿却找不到原因,那不妨试试本文的排查方法吧,说不定就是内存泄漏引起的,同时,它也是我们在做页面优化时需要额外注意的一个点。
今天就到这里了,你 GET 到了吗?欢迎指误堪错!写作不易,动动小手点个赞吧,收藏吃灰是大忌!
本文作者:isboyjc
原文链接:点击这里
更多 建站教程 请访问 https://www.mimiwuqi.com/webqianduan/