Rumah >hujung hadapan web >tutorial js >Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

藏色散人
藏色散人ke hadapan
2023-02-08 11:05:222270semak imbas

Penutupan adalah kesukaran utama dalam JS; terdapat banyak penerangan di Internet tentang penutupan yang menyebabkan kebocoran memori, mengatakan bahawa penutupan akan menjadikan nilai pembolehubah sentiasa Dipelihara; dalam ingatan, secara amnya tidak disyorkan untuk menggunakan penutupan

Tetapi memang terdapat banyak senario di mana penutupan digunakan dalam projek, seperti pendikit fungsi dan anti goncang

Jadi penutupan digunakan lebih banyak , adakah ia akan menyebabkan kebocoran ingatan?

Pemikiran senario

Kes berikut: Halaman A memperkenalkan fungsi anti-goncang debounce Selepas melompat ke halaman B, memori yang diduduki oleh penutupan dalam fungsi anti-goncang akan menjadi gc Kitar Semula?

Dalam kes ini, 变异版的防抖函数 digunakan untuk menunjukkan kitar semula memori penutupan Fungsi ini merujuk kepada objek dengan memori yang besar info (memori 42M), yang memudahkan untuk membandingkan perubahan dengan jelas. dalam ingatan sebelum dan selepas

Nota: Anda boleh menggunakan alat Memori Chrome untuk menyemak saiz memori halaman:

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

Langkah senario:

1) util.js Sederhana debounce fungsi anti goncang

// 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) diperkenalkan dan digunakan dalam halaman A

import { debounce } from './util';mounted() {    this.debounceFn = debounce(() => {      console.log('1');
    }, 1000)
}
  • 抓取 A 页面内存: 57.1M
Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

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

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

  • 此时,抓取 B 页面内存: 58.1M
Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori
  • 刷新 B 页面,该页面的原始内存为: 16.1M
Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

结论: 前后对比发现,从 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 滞留在内存中

打断点调试一下

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

展开函数 f 的 Scope(作用域的意思)选项,会发现有 Local 局部作用域、Closure 闭包、Global 全局作用域等值,展开 Closure,会发现该闭包被访问的变量是 num,包含 num 的函数为 fn

总结来说,函数 f 的作用域中,访问到了fn 函数中的 num 这个局部变量,从而形成了闭包

所以,如果真正理解好闭包,需要先了解闭包的内存引用,并且要先搞明白这几个知识点:

  • 函数作用域链
  • 执行上下文
  • 变量对象、活动对象

函数的内存表示

先从最简单的代码入手,看下变量是如何在内存中定义的

let a = '小马哥'

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

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

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

再定义一个函数

let a = '小马哥'function fn() {  let num = 1}

内存结构如下:

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

特别注意的是,fn 函数中有一个 [[scopes]] 属性,表示该函数的作用域链,该函数作用域指向全局作用域(浏览器环境就是 window),函数的作用域是理解闭包的关键点之一

请谨记:函数的作用域链是在创建时就确定了,JS 引擎会创建函数时,在该对象上添加一个名叫作用域链的属性,该属性包含着当前函数的作用域以及父作用域,一直到全局作用域

函数在执行时,JS 引擎会创建执行上下文,该执行上下文会包含函数的作用域链(上图中红色的线),其次包含函数内部定义的变量、参数等。在执行时,会首先查找当前作用域下的变量,如果找不到,就会沿着作用域链中查找,一直到全局作用域

垃圾回收机制浅析

现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数

这里重点介绍 "引用计数"(reference counting),JS 引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

上图中,左下角的两个值,没有任何引用,所以可以释放

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏

判断一个对象是否会被垃圾回收的标准: 从全局对象 window 开始,顺着引用表能找到的都不是内存垃圾,不会被回收掉。只有那些找不到的对象才是内存垃圾,才会在适当的时机被 gc 回收

分析内存泄露的原因

回到最开始的场景,当 info 在 debounce 函数外部时,为什么会造成内存泄露?

进行断点调试

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

展开 debounce 函数的 Scope选项,发现有两个 Closure 闭包对象,第一个 Closure 中包含了 info 对象,第二个 Closure 闭包对象,属于 util.js 这个模块

内存结构如下:

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

当从 A 页面切换到 B 页面时,A 页面被销毁,只是销毁了 debounce 函数当前的作用域,但是 util.js 这个模块的闭包却没有被销毁,从 window 对象上沿着引用表依然可以查找到 info 对象,最终造成了内存泄露

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

当 info 在 debounce 函数内部时,进行断点调试

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

其内存结构如下:

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

当从 A 页面切换到 B 页面时,A 页面被销毁,同时会销毁 debounce 函数当前的作用域,从 window 对象上沿着引用表查找不到 info 对象,info 对象会被 gc 回收

Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori

闭包内存的释放方式

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视频教程

Atas ialah kandungan terperinci Artikel yang menganalisis sama ada menggunakan terlalu banyak penutupan akan menyebabkan kebocoran memori. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:juejin.im. Jika ada pelanggaran, sila hubungi admin@php.cn Padam