>  기사  >  웹 프론트엔드  >  클로저를 너무 많이 사용하면 메모리 누수가 발생하는지 분석한 기사

클로저를 너무 많이 사용하면 메모리 누수가 발생하는지 분석한 기사

藏色散人
藏色散人앞으로
2023-02-08 11:05:222215검색

클로저는 JS의 주요 난제입니다. 인터넷에는 클로저가 메모리 누수를 유발한다는 설명이 많이 있으며, 클로저는 변수의 값을 메모리에 유지하므로 일반적으로 권장되지 않습니다. 그리고 기능 제한, 흔들림 방지 등 프로젝트에서 클로저가 사용되는 시나리오가 실제로 많이 있습니다.

그렇다면 클로저를 너무 많이 사용하면 메모리 누수가 발생할까요?

시나리오에 대한 생각

다음 사례: 페이지 A는 페이지 B로 점프한 후 흔들림 방지 기능의 클로저가 차지하는 메모리를 디바운스 흔들림 방지 기능을 도입합니다. GC?

이 경우 손떨림 방지 기능의 변형된 버전은 클로저의 메모리 재활용을 시연하는 데 사용됩니다. 이 함수는 대규모 메모리 개체 info(42M)를 참조합니다. debounce 防抖函数,跳转到 B 页面后,该防抖函数中闭包所占的内存会被 gc 回收吗?

该案例中,通过变异版的防抖函数来演示闭包的内存回收,此函数中引用了一个内存很大的对象 info(42M的内存),便于明显地对比内存的前后变化

注:可以使用 Chrome 的 Memory 工具查看页面的内存大小:

클로저를 너무 많이 사용하면 메모리 누수가 발생하는지 분석한 기사

场景步骤:

1) util.js 中定义了 debounce

참고: Chrome의 메모리 도구를 사용하여 페이지의 메모리 크기를 볼 수 있습니다:

클로저를 너무 많이 사용하면 메모리 누수가 발생하는지 분석한 기사

장면 단계:🎜🎜1 ) util.js <code>debounce 흔들림 방지 기능은 🎜
// 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) 흔들림 방지 기능은 페이지 A🎜에 도입되어 사용됩니다.
import { debounce } from &#39;./util&#39;;mounted() {    this.debounceFn = debounce(() => {      console.log(&#39;1&#39;);
    }, 1000)
}
  • 抓取 A 页面内存: 57.1M
클로저를 너무 많이 사용하면 메모리 누수가 발생하는지 분석한 기사

3) 从 A 页面跳转到 B 页面,B 页面中没有引入该 debounce 函数

问题: 从 A 跳转到 B 后,该函数所占的内存会被释放掉吗?

  • 此时,抓取 B 页面内存: 58.1M
클로저를 너무 많이 사용하면 메모리 누수가 발생하는지 분석한 기사
  • 刷新 B 页面,该页面的原始内存为: 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 = &#39;小马哥&#39;

这样一段代码,在内存里表示如下

클로저를 너무 많이 사용하면 메모리 누수가 발생하는지 분석한 기사

在全局环境 window 下,定义了一个变量 a,并给 a 赋值了一个字符串,箭头表示引用

再定义一个函数

let a = &#39;小马哥&#39;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视频教程

위 내용은 클로저를 너무 많이 사용하면 메모리 누수가 발생하는지 분석한 기사의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.im에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제