花费28800秒,带你深入浅出JS中的弱引用 垃圾回收 WeakMap FinalizationRegistry

1. 背景

最近在写深拷贝的时候,发现大部分文章都会使用 WeakMap 来解决依赖循环的问题,那么为什么都去选择 WeakMap,以及 WeakMap 背后代表的弱引用究竟是什么意思?通过阅读本文章可以收获:

  • 关于 gc 的相关知识
  • FinalizationRegistry API 的使用
  • 从内存图的角度来说明 WeakMap 代表的 弱引用 究竟是意思
  • WeakSetWeakRef 的使用
  • 弱引用的应用示例:DOM节点属性缓存、数据缓存
  • ......

注意 :本文大部分实验都位于 Nodejs 18 的版本来做的处理,可能各个版本存在部分差异

2. 讲下 Java 的引用

大家都知道高级语言的其实都是相通的,特别是高级语言,比如隔壁的 Java(doge),可以看到 Java 分为了下面的 4个引用方式

我们再来看下 Java 如何去定义弱引用

弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

可以发现和 JavaScript 从概念的角度其实是一致的。对于 GC 来说,只有弱引用会对该对象进行清除操作

3 Nodejs如何去执行GC

首先我们需要知道如何使用 Nodejs 来手动执行 GC,以及手动执行的一些坑

Nodejs 是可以手动执行 gc 的,但是我在执行出现问题了,这是因为 Nodejs 不允许我们手动来执行。我们需要手动携带参数进去,告诉 Nodejs 可以显式的去执行

我们带上 --expose-gc 即可手动执行 gc 操作

4. FinalizationRegistry

我们在介绍弱引用之前还需要介绍一下 FinalizationRegistry 这个API,他是 ES12 新加的,FinalizationRegistry 允许我们注册一个回调函数,当注册的值被垃圾回收时,这个回调函数会被调用。这样我们就可以直观的看到哪些参数被回收了

具体可以上 MDN 查看详细使用:FinalizationRegistry - JavaScript | MDN (mozilla.org)

  • 使用构造函数创建,并且传递一个回调函数,当注册的对象被垃圾回收就会执行该回调函数
  • 使用 register 来注册对象,后面可以传递参数,该参数会传递给回调函数
  • 使用 register 注册的对象进行的引用时弱引用,这样时符合逻辑的,不然就无法被垃圾回收了
  • 使用 unregister 即可取消注册,传入注册时的对象

这样我们就可以很直观的看到哪些对象被垃圾回收了

还需要注意一个点,我们来看下面的例子:为什么不能被移除!我都添加了这么多东西,按理说 person 没用到,自然会被清除吧!

这里不能惯性思维了!(大佬一眼就看出来了,其实我一开始惯性思维了,琢磨了半天,其实很简单,呜呜呜~~~

我们来手动清除呢?可以看到已经被清除了

我们来解释一下上图为什么会出现这个问题,因为他在全局的作用域中,可能随时都需要使用,所以并不会清除掉,我们来试一下函数作用域

欸,这不是没清除吗?别着急,因为垃圾清理是需要触发机制的,我们再请出我们的 垃圾制造机 allocateMemory 来触发 GC,这个函数之后会频繁用到

可以看到在函数作用域的情况下,函数执行完毕就会将不需要使用的变量标记为 不活跃的对象,随后垃圾回收就会将这部分数据清除掉

我们再来看 MDN 对于 GC 的解释,可以看出 GC 的实现其实也可能存在不同

GC 在一个 JavaScript 引擎中的行为有可能在另一个 JavaScript 引擎中的行为大相径庭,或者甚至在同一类引擎,不同版本中 GC 的行为都有可能有较大的差距

5. WeakMap / WeakSet

5.1 WeakMap

我们就以 WeakMap 为切入点来介绍一下弱引用,同时编写如下代码:

  • 我们创建 WeakMap 和 Map ,并且传入 key1 和 key2 对象分别作为 map1 和 map2key,来彼此做对照实验
  • 设置完成之后,将 key1 和 key2 赋值为 null,解除对它们的引用
  • 手动执行 gc 来清除不活跃的对象,再来查看输出的格式,可以看到依旧输出了

有长的帅的要说了:"为啥 WeakMap< item unknown > 啊!看着好难受啊,能不能解决呢?"其实是可以的,我们引入 inspect 即可打印 WeakMap 中的值

解决方案:javascript - ES6: console.log on WeakSet gives - Stack Overflow

inspect文档:Util | Node.js v20.12.1 Documentation (nodejs.org)

javascript 复制代码
const { inspect } = require("util");

const fr = new FinalizationRegistry((value) => {
  console.log(`${value} 于 ${new Date().getTime()} 被垃圾回收掉了~`);
});

// 执行 key1
let key1 = { name: "key1" };
const map1 = new WeakMap();
map1.set(key1, "key1");

// 执行 key2
let key2 = { name: "key2" };
const map2 = new Map();
map2.set(key2, "key2");

// 注册 FinalizationRegistry
fr.register(key1, "key1");
fr.register(key2, "key2");

// 手动释放 key1 和 key2
key1 = null;
key2 = null;

console.log(inspect(map1, { showHidden: true }));
console.log(inspect(map2, { showHidden: true }));

console.log("============================================");

// 手动执行gc
gc();

console.log(inspect(map1, { showHidden: true }));
console.log(inspect(map2, { showHidden: true }));

我们通过下面的输出结果可以对比出结果,可以发现 Map 中的引用依旧存在,而 WeakMap 中的引用已经消失了,这就是弱引用和强引用核心的区别

我这里画了一个内存图来供大家查看,这是存储时的结构内存图(注意:JS引擎不同,实现的原理也不一定相同,下面只做原理解释,并非实际情况)

  • 可以看到 key1 和 key2 都是引用类型,所以只存储地址值,实际的数据在别处
  • map1 和 map2 也是一样,存储的 键(key) 存储的地址值来做的引用
  • map1 作为 WeakMap弱引用 引用着 key1 ,也就是下图的 0x123

当我们把 key1 和 key2 赋值为 null 的时候

  • 可以发现 key1 和 key2 对于地址值为 0x123 和 0x456 的引用没了 ,只剩下 map1 和 map2 的引用了
  • 这个时候手动执行 gc 操作,key1 和 key2 会被清除
  • map1 对于 key1(0x123) 是弱引用,在垃圾清除的时候会被清除掉,而 map2 的 依旧引用着 key2,所以没有被删除

最终的结果可以参考下图:map1 已经没数据了,而 map2 依旧强引用这 key2 的地址,所以没有被清除

根据上面的内存图的解释,是不是更加明白了弱引用的作用了。弱引用可以去建立连接,但是他不保证该引用被清除(你把它当作渣男,是不是就好理解了 doge )

好,上面的知道了,下面我们再来一个示例来看看

  • person 是一个引用类型,其中 friends 也是一个引用类型
  • map1 和 map2 都是 WeakMap 分别存储 personfriends

如果这个时候把 person 设置为 null 的时候,内存会如何变化

  • person 的引用断开了,只剩下 WeakMap 的 key 的引用
  • 如果执行 gc 的话,2个对象都会被清除,我们来执行一下结果,看下是不是的

可以看到和预期一致,引用全部断开

5.2 WeakSet

如果你知道了 WeakMap 的解释,那么 WeakSet 也是一样的,这里就不画图了,直接放例子即可

  • 创建 person 引用类型 和 WeakSet ,将 person 放进 WeakSet
  • 我们手动取消 person 的引用,并且手动执行 gc ,可以看到 WeakSet 和 Person 都被清理掉了

6. WeakRef

我们来介绍一下 WeakRef ,这个 API 使用的比较少,也不推荐使用,其原理看懂了上面的 WeakMap 就知道它如何去使用了

  • 使用 WeakRef 来引用 person,这样来建立弱引用,随后便分别取消引用,赋值为 null
  • 使用 process.memoryUsage() 来监听内存变化,主要看 heapUsed 即可,在执行完 gc 之后内存确实减少了
  • 执行完 gc 之后,weakRef 可以被释放掉,但是 person 不能被释放掉,也可能是已经释放掉了,但是 FinalizationRegistry 并没有监听到而已(浏览器环境可以被释放掉

注意 :我在 Nodejs 的环境中始终无法去释放 WeakRef 引用的对象,但是在浏览器环境中可以正常的释放,我暂时不知道是因为什么原因导致的差异,如果有知道的可以评论区留言,感谢大佬们!!!

我们来看下浏览器环境 的变化,我来手动去控制台执行 gc,可以看到 person 和 weakref 都可以被释放掉

同时在 MDN 中也给出了 Avoid where possible 的警告,告诉我们最好不要使用 WeakRef 这个 API,这个我就当作了解了吧!

7. 应用

我们知道了弱引用是啥!那么我们在那里可以应用到呢?老实说,在平常的业务中使用的频率不算很高,都有更加熟悉的处理方式。大部分都是主前端的项目或者框架级别的项目,去使用这个特性。如果硬是要用的话,可以总结为数据缓存

7.1 DOM属性缓存

我看了很多网上 DOM节点 的示例,一直没发现特别能说服自己的,我这里就引用 张鑫旭 大佬的博客中提到点作为参考想了一个示例

博客地址:JS WeakMap应该什么时候使用 << 张鑫旭-鑫空间-鑫生活 (zhangxinxu.com)

在看 DOM属性缓存之前先看一下下面的代码

  • 我们对 .main 进行了点击点击监听,当我们点击之后便会删除 .main 自身节点
  • 删除之后,我们来循环打印 mainEl ,可以发现 节点 被删除了,但是节点的数据依旧保持着引用,我们是可以通过这个节点来复原
  • 如果你想删除引用只能手动将 mainEl 设置 null,然后让 gc 来做清除

知道了上面的点之后,我们再来看下面的代码

  • 我添加了一个 .main 并且使用 FinalizationRegistry 来监听 .main 的引用 mainEl 是否被垃圾回收了
  • 使用 WeakMap 来缓存 mainEl 的各类属性,这样我们可以为该节点添加各种各样的初始化属性了
  • mainEl 添加事件监听,如果我们点击之后,就会删除 mainEl 自身 。同时也添加一个 button 的点击事件,点击之后就会使用 WeakMap 中保存的初始化属性
  • 在删除节点之后,疯狂扩展内存,手动触发浏览器的 垃圾回收 机制,来查看输出的结果
html 复制代码
<div class="main">
  <div class="item">列表元素</div>
  <div class="item">列表元素</div>
  <div class="item">列表元素</div>
</div>
<button class="apply" style="margin-top: 10px">应用属性</button>

<script>
  const fr = new FinalizationRegistry((value) => {
    console.log(`${value} 于 ${new Date().getTime()} 被垃圾回收掉了~`);
  });

  let mainEl = document.querySelector(".main");

  // 使用 WeakMap 为 main 添加属性
  const wm = new WeakMap();
  wm.set(mainEl, {
    style: {
      textColor: "blue",
      backgroundColor: "red",
    },
    attr: {
      name: "wrapper-main",
    },
  });

  // 注册监听,如果 mainEl 被垃圾回收就执行回调函数
  fr.register(mainEl, "mainEl");

  // 对 main 进行事件监,点击之后删除 main 节点
  const removeMainEl = () => {
    // 解除绑定,删除元素数据
    mainEl.removeEventListener("click", removeMainEl);
    mainEl.remove();
    mainEl = null;

    allocateMemory();
  };
  mainEl.addEventListener("click", removeMainEl);

  // 对 apply 进行事件监听,点击之后为 mainEl 添加属性
  const applyEl = document.querySelector(".apply");
  applyEl.addEventListener("click", () => {
    if (wm.has(mainEl)) {
      const { style, attr } = wm.get(mainEl);

      mainEl.style.backgroundColor = style.backgroundColor;
      mainEl.style.color = style.textColor;
      mainEl.setAttribute("name", attr.name);
    }
  });

  // 疯狂扩展内存,激活 V8 的 GC
  function allocateMemory() {
    Array.from({ length: 50000 }, () => () => {});
    setInterval(allocateMemory, 1000);
    console.log("V8:累死我了,数组添加成功~");
  }
</script>

代码已经准备好了,我们来分析一下 WeakMap 在这段代码中做了什么?

之前说过,WeakMap 是弱引用可以用作数据缓存,所以对于 mainEl(.main节点) 也是弱引用,这样 mainEl 只要被清除,那么 mainEl 作为 key 也就不存在了,所以背后的 value 也就不存在了,这样会被一起释放掉,这样就对 mainEl 节点的属性做了缓存初始值的操作。也就是节点存在属性就存在,节点消失属性也消失了,这样我们就不需要再来手动清除节点属性的引用

注意 :可能各个浏览器存在差异,存在差异,存在差异!!! 我在 Edge 123 版本中控制台无法监听到 mainEl 被释放掉了,其实已经被释放掉了,只不过没在控制台输出,必须手动点击浏览器才能恢复,但是在 谷歌浏览器 中是正常运行,要运行这段代码记得操作一下

7.2 数据缓存

在一些情况下,对象中的一些数据我们需要存储管理,而这些数据和对象没什么关联,不好直接丢到对象中,我们就可以将这些数据传递进 WeakMap 中,我们来看下面的示例:

  • 创建一个对象 person,它有一些不完整的数据,我们可以手动来做处理
  • 创建一个 setCustom 函数,它可以计算出 key 的个数,还可以记录 learnFactory 函数的返回结果
  • 我们执行 get 就可以获取这个对象的额外属性,这样可以避免很多的麻烦,提前对部分数据做缓存,需要的时候取出即可
  • 如果我们将 person 设置为 null 的时候,那么 垃圾回收 会做处理,WeakMap 的 key 消失,其引用的 value 也会随之被清理

8. 总结

我通过绘制内存图的方式给大家讲解什么是弱引用,同时介绍了:

  • Java中的引用:通过介绍Java中的弱引用,为理解JavaScript中的弱引用提供了基础
  • Node.js的GC:文章解释了如何在Node.js中手动执行垃圾回收
  • FinalizationRegistry:介绍了FinalizationRegistry API,介绍了该 API 如何使用
  • WeakMap和WeakSet:通过创建WeakMap并与Map对比实验,展示了弱引用和引用的区别。WeakMap中的键(key)是弱引用的,当键不再被其他变量引用时,垃圾回收器可以回收其关联的对象
  • WeakRef:简要介绍了WeakRef API,并提到在Node.js环境中遇到了无法释放WeakRef引用对象的问题,而在浏览器环境中则可以正常工作,并且尽量避免使用WeakRef

------ GPT AI

写原创文章不易!!!如果讲的好,请给出你手动的点赞 ,如果文章有问题,可以在评论区留言哦 🤠🤠🤠

参考文章

强引用、软引用、弱引用和虚引用 - 掘金 (juejin.cn)

WeakMap - JavaScript | MDN (mozilla.org)

WeakMap and WeakSet(弱映射和弱集合) (javascript.info)

《JavaScript高级程序设计(第4版)》

一文让你彻底搞懂JS垃圾回收机制 - 掘金 (juejin.cn)

相关推荐
阿伟来咯~37 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端42 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨1 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js