首頁  >  文章  >  web前端  >  一文深析閉包用多是否會造成記憶體洩露

一文深析閉包用多是否會造成記憶體洩露

藏色散人
藏色散人轉載
2023-02-08 11:05:222228瀏覽
##.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933 } .markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin - bottom:10px;padding-bottom:5px}.markdown-body h1{font-size:24px;line-height:38px;margin-bottom:5px}.markdown-body h2{font-size:22px;line-height : 34px;padding-bottom:12px;border-bottom:1pxsolid #ececec}.markdown-body h3{font-size:20px;line-height:28px}.markdown-body h4{font-size:18px;line-高度: 26px}.markdown-body h5{font-size:17px;line-height:24px}.markdown-body h6{font-size:16px;line-height:24px}.markdown-body p{line-height:繼承; margin-top:22px;margin-bottom:22px}.markdown-body img{max-width:100%}.markdown-body hr{border:none;border-top:1pxsolid #ddd;margin-top: 32px;margin -bottom:32px}.markdown-body程式碼{word-break:break-word;border-radius:2px;overflow-x:auto;background-color:#fff5f5;color:#ff502c;font-size: .87em;填充:.065em .4em}.markdown-body程式碼,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative ;line- height:1.75}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333 ;背景: #f8f8f8}.markdown-body a{text-decoration:none;color:#0269c8;border-bottom:1pxsolid #d1e9ff}.markdown-body a:active,.markdown-body a:hover{顏色: #275b8c}. markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1pxsolid #f6f6f6}.markdown-body thead {背景:#f6f6f6 ;顏色:#000;text-align:left}.markdown-body tr:nth-child(2n){背景顏色:#fcfcfc}.markdown-body td,.markdown-body th{填充:12px 7px;line- height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:4pxsolid # cbcbcb; }.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body ol,.markdown-body ul {padding-left:28px}. markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item {list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li. 任務清單項目ol,.markdown- body ul li .任務清單項目ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown -body ul ul{margin-top:3px}. markdown-body ol li{padding-left:6px}.markdown-body .contains-task-list{padding-left:0}.markdown-body .task-list- item{list-style:none}@media (max -width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-大小:18px}}. markdown-body pre,.markdown-body pre>code.hljs{顏色:#a9b7c6;背景:#282b2e}.hljs-bullet,.hljs-literal,.hljs-數字,.hljs-符號{顏色:#6897bb} .hljs-刪除,.hljs-關鍵字,.hljs-選擇器-標籤{顏色:#cc7832}.hljs-連結,.hljs-範本變數,.hljs-變數{顏色:#629755}.hljs-comment, .hljs-quote{顏色:灰色}.hljs-meta{顏色:#bbb529}.hljs-addition,.hljs-attribute,.hljs-string{顏色:#6a8759} .hljs-section,.hljs-title,. hljs-type{color:#ffc66d}.hljs-name,.hljs-selector-class,.hljs-selector-id{color:#e8bf6a}.hljs-emphasis{font -style:italic}.hljs-strong{font -weight:700}閉包,是JS中的一大難點;網路上有很多關於閉包會造成記憶體外洩的描述,說閉包會導致中的變數的值始終保持在記憶體中,一般都不太推薦使用閉包

而專案中確實有很多使用閉包的場景,例如函數的節流與防手震

那麼閉包用多了,會造成記憶體洩漏嗎?

場景思考

以下案例: A 頁面引入了一個

debounce 防手震函數,截圖到B頁後,該防手震函數中閉包所佔的記憶體會被gc恢復嗎?

該案例中,透過

變異版的防抖函數來示範閉包的記憶體恢復,此函數中引用了一個記憶體很大的物件info(42M的記憶體),由此明顯比較記憶體的前後變化

#註:可以使用Chrome的記憶體工具檢視頁面的記憶體大小:

一文深析閉包用多是否會造成記憶體洩露

場景步驟:

#1)

util.js 中定義了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 './util';mounted() {    this.debounceFn = debounce(() => {      console.log('1');
    }, 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 = '小马哥'

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

一文深析閉包用多是否會造成記憶體洩露

在全局环境 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视频教程

以上是一文深析閉包用多是否會造成記憶體洩露的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除