Maison > Article > interface Web > Un article analysant si l'utilisation d'un trop grand nombre de fermetures entraînerait des fuites de mémoire
Les fermetures sont une difficulté majeure en JS ; il existe de nombreuses descriptions sur Internet de fermetures provoquant des fuites de mémoire, disant que les fermetures conserveront les valeurs des variables en mémoire, et ne sont généralement pas recommandées. Et il existe en effet de nombreux scénarios dans lesquels des fermetures sont utilisées dans des projets, comme la limitation de fonctions et l'anti-shake
L'utilisation de trop de fermetures entraînera-t-elle donc des fuites de mémoire ?
Penser aux scénarios
debounce
Après avoir sauté à la page B, la mémoire occupée par la fermeture dans la fonction anti-shake sera-t-elle récupérée par. gc ? Dans ce cas, la version mutée de la fonction anti-shake
est utilisée pour démontrer le recyclage mémoire des fermetures. Cette fonction fait référence à un gros objet mémoire info
(42M). mémoire) pour comparer clairement les changements de mémoire avant et après debounce
防抖函数,跳转到 B 页面后,该防抖函数中闭包所占的内存会被 gc 回收吗?
该案例中,通过变异版的防抖函数
来演示闭包的内存回收,此函数中引用了一个内存很大的对象 info
(42M的内存),便于明显地对比内存的前后变化
注:可以使用 Chrome 的 Memory 工具查看页面的内存大小:
场景步骤:
1) util.js
中定义了 debounce
util.js La fonction anti-shake <code>debounce
est définie dans 🎜// util.js`let info = { arr: new Array(10 * 1024 * 1024).fill(1), timer: null};export const debounce = (fn, time) => { return function (...args) { info.timer && clearTimeout(info.timer); info.timer = setTimeout(() => { fn.apply(this, args); }, time); }; };🎜2) La fonction anti-shake est introduite et utilisée dans la page A🎜
import { debounce } from './util';mounted() { this.debounceFn = debounce(() => { console.log('1'); }, 1000) }
57.1M
3) 从 A 页面跳转到 B 页面,B 页面中没有引入该 debounce 函数
问题: 从 A 跳转到 B 后,该函数所占的内存会被释放掉吗?
58.1M
16.1M
结论: 前后对比发现,从 A 跳转到 B 后,B 页面内存增大了 42M
,证明该防抖函数所占的内存没有被释放掉,即造成了内存泄露
为什么会这样呢? 按理说跳转 B 页面后,A 页面的组件都被销毁掉了,那么 A 页面所占的内存应该都被释放掉了啊?
我们继续对比测试
4) 如果把 info 对象放到 debounce 函数内部,从 A 跳转到 B 后,该防抖函数所占的内存会被释放掉吗?
// util.js`export const debounce = (fn, time) => { let info = { arr: new Array(10 * 1024 * 1024).fill(1), timer: null }; return function (...args) { info.timer && clearTimeout(info.timer); info.timer = setTimeout(() => { fn.apply(this, args); }, time); }; };
按照步骤 4 的操作,重新从 A 跳转到 B 后,B 页面抓取内存为16.1M
,证明该函数所占的内存被释放掉了
为什么只是改变了 info 的位置,会引起内存的前后变化?
要搞懂这个问题,需要理解闭包的内存回收机制
闭包:一个函数内部有外部变量的引用,比如函数嵌套函数时,内层函数引用了外层函数作用域下的变量,就形成了闭包。最常见的场景为:函数作为一个函数的参数,或函数作为一个函数的返回值时
闭包示例:
function fn() { let num = 1; return function f1() { console.log(num); };} let a = fn();a();
上面代码中,a 引用了 fn 函数返回的 f1 函数,f1 函数中引入了内部变量 num,导致变量 num 滞留在内存中
打断点调试一下
展开函数 f 的 Scope(作用域的意思)选项,会发现有 Local 局部作用域、Closure 闭包、Global 全局作用域等值,展开 Closure,会发现该闭包被访问的变量是 num,包含 num 的函数为 fn
总结来说,函数 f 的作用域中,访问到了fn 函数中的 num 这个局部变量,从而形成了闭包
所以,如果真正理解好闭包,需要先了解闭包的内存引用,并且要先搞明白这几个知识点:
先从最简单的代码入手,看下变量是如何在内存中定义的
let a = '小马哥'
这样一段代码,在内存里表示如下
在全局环境 window 下,定义了一个变量 a,并给 a 赋值了一个字符串,箭头表示引用
再定义一个函数
let a = '小马哥'function fn() { let num = 1}
内存结构如下:
特别注意的是,fn 函数中有一个 [[scopes]] 属性,表示该函数的作用域链,该函数作用域指向全局作用域(浏览器环境就是 window),函数的作用域是理解闭包的关键点之一
请谨记:函数的作用域链是在创建时就确定了,JS 引擎会创建函数时,在该对象上添加一个名叫作用域链的属性,该属性包含着当前函数的作用域以及父作用域,一直到全局作用域
函数在执行时,JS 引擎会创建执行上下文,该执行上下文会包含函数的作用域链(上图中红色的线),其次包含函数内部定义的变量、参数等。在执行时,会首先查找当前作用域下的变量,如果找不到,就会沿着作用域链中查找,一直到全局作用域
现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数
这里重点介绍 "引用计数"(reference counting),JS 引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0
,就表示这个值不再用到了,因此可以将这块内存释放
上图中,左下角的两个值,没有任何引用,所以可以释放
如果一个值不再需要了,引用数却不为0
,垃圾回收机制无法释放这块内存,从而导致内存泄漏
判断一个对象是否会被垃圾回收的标准: 从全局对象 window 开始,顺着引用表能找到的都不是内存垃圾,不会被回收掉。只有那些找不到的对象才是内存垃圾,才会在适当的时机被 gc 回收
回到最开始的场景,当 info 在 debounce 函数外部时,为什么会造成内存泄露?
进行断点调试
展开 debounce 函数的 Scope选项,发现有两个 Closure 闭包对象,第一个 Closure 中包含了 info 对象,第二个 Closure 闭包对象,属于 util.js 这个模块
内存结构如下:
当从 A 页面切换到 B 页面时,A 页面被销毁,只是销毁了 debounce 函数当前的作用域,但是 util.js 这个模块的闭包却没有被销毁,从 window 对象上沿着引用表依然可以查找到 info 对象,最终造成了内存泄露
当 info 在 debounce 函数内部时,进行断点调试
其内存结构如下:
当从 A 页面切换到 B 页面时,A 页面被销毁,同时会销毁 debounce 函数当前的作用域,从 window 对象上沿着引用表查找不到 info 对象,info 对象会被 gc 回收
1、手动释放(需要避免的情况)
如果将闭包引用的变量定义在模块中,这种会造成内存泄露,需要手动释放,如下所示,其他模块需要调用 clearInfo 方法,来释放 info 对象
可以说这种闭包的写法是错误的 (不推荐), 因为开发者需要非常小心,否则稍有不慎就会造成内存泄露,我们总是希望可以通过 gc 自动回收,避免人为干涉
let info = { arr: new Array(10 * 1024 * 1024).fill(1), timer: null};export const debounce = (fn, time) => { return function (...args) { info.timer && clearTimeout(info.timer); info.timer = setTimeout(() => { fn.apply(this, args); }, time); }; };export const clearInfo = () => { info = null; };
2、自动释放(大多数的场景)
闭包引用的变量定义在函数中,这样随着外部引用的销毁,该闭包就会被 gc 自动回收 (推荐),无需人工干涉
export const debounce = (fn, time) => { let info = { arr: new Array(10 * 1024 * 1024).fill(1), timer: null }; return function (...args) { info.timer && clearTimeout(info.timer); info.timer = setTimeout(() => { fn.apply(this, args); }, time); }; };
综上所述,项目中大量使用闭包,并不会造成内存泄漏,除非是错误的写法
绝大多数情况,只要引用闭包的函数被正常销毁,闭包所占的内存都会被 gc 自动回收。特别是现在 SPA 项目的盛行,用户在切换页面时,老页面的组件会被框架自动清理,所以我们可以放心大胆的使用闭包,无需多虑
理解了闭包的内存回收机制,才算彻底搞懂了闭包。以上关于闭包的理解,如果有误,欢迎指正
推荐学习:《JavaScript视频教程》
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!