编程技术文章分享与教程

网站首页 > 技术文章 正文

「译」通过内存快照看WeakMap与三个典型应用?

hmc789 2024-11-18 12:55:09 技术文章 1 ℃

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

前言

自 2015 年 6 月 ES2015 发布以来已经过去了很多年。在撰写之前的每周精选时,我在想“是否有我没有使用到的 ES2015 语法,例如 Proxy 或 Reflect? ",首先想到的是 WeakMap 和 WeakSet。因为我对弱引用这个话题比较感兴趣,所以就着这个话题我准备写一篇 WeakMap 的文章来带着大家一起来了解下它。

1.什么是 WeakMap?

WeakMap 对象是键/值对的集合,其中的键被弱引用。 键必须是对象,原始值不能是键。 如果你添加一个原始值作为键,你会得到一个错误 :

Uncaught TypeError: Invalid value used as weak map key。

WeakMap 提供了一种检查特定键是否有值的方法,但它没有提供枚举键对象的方法。 众所周知,在 WeakMap 中用作键的对象需要进行垃圾回收。 如果除 WeakMap 外,程序中不存在对对象的引用,则该对象将被垃圾回收。假设有一个使用闭包返回长数组的函数,如下所示:

// refs:
// https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots/
function createLargeClosure() {
  const largeStr = new Array(1000000).join('x');
  const lc = function largeClosure() {
    return largeStr;
  };
  return lc;
}

首先,使用 Map 创建一个内存泄漏示例:

const map = new Map();
function start() {
  const timer = setInterval(() => {
    const lc = createLargeClosure();
    // 函数
    map.set(lc, '##');
  }, 1000);
  setTimeout(function () {
    clearInterval(timer);
  }, 5001);
}

使用 DevTools 检查内存分配状态:

现在,如果用 new WeakMap() 而不是 new Map() 替换集合对象后运行相同的代码,可以看到内存被释放。

如果您因为不熟悉 DevTools 而对这个快照不熟悉,建议您观看“Chrome DevTools 详解”或阅读 Chrome 文档,统一提供在参考文献中。

2.WeakMap 构造函数的特点

  • WeakMap 是全局对象的一个属性
  • 不能作为通用函数调用,作为通用函数调用时会报错
  • 必须使用 new 关键字调用构造函数。
  • 它有一个内部插槽 [[Prototype]],它的值是 Function.prototypeWeakMap.prototype.constructor.proto === Function.prototype;
  • 它可以通过 extends 关键字用于继承。

3.WeakMap 方法

WeakMap 只提供四种方法,下面会讲到。 如前所述,不支持枚举键相关的方法。 根据 ECMAScript 规范,WeakMap 的实现在键/值对变得不可访问和键/值对从 WeakMap 中删除之间可能会有延迟。 因此,无法执行涉及枚举整个键/值对的操作。

3.1 WeakMap.prototype.delete(key)

删除映射到键的所有值, WeakMap.prototype.has(key) 之后会返回 false。

3.2 WeakMap.prototype.get(key)

返回映射到 key 的值,如果没有映射值则返回未定义。

3.3 WeakMap.prototype.has(key)

返回一个布尔值,表示映射到 WeakMap 实例对象中的键的值是否存在。

3.4 WeakMap.prototype.set(key, value)

在 WeakMap 实例对象中设置映射到 key 的值(value),返回 WeakMap 实例对象。

3.5 WeakMap.prototype.clear()

由于安全问题,除了 IE 之外的所有浏览器都弃用了 clear() 方法。当前版本或者起草中没有这个方法,这个方法在版本 28(2014 年 10 月 14) 之前是 ECMAScript 6 起草规范的一部分,但是在起草之后的版本中被移除了。它不在是最终标准的一部分了 。

4.WeakMap 示例

WeakMap 的简单介绍到此结束,接下来看几个例子熟悉下如何使用它。通常,WeakMap 用于容易发生内存泄漏的代码。

4.1 Caching(缓存)

WeakMap 有助于数据存储(memoization),数据存储(memoization)是一种缓存昂贵计算的结果并在接收到相同输入值时返回缓存结果的技术。

function createLargeClosure() {
  const largeObj = {
    a: 1,
    b: 2,
    str: new Array(1000000).join('x'),
  };
  const lc = function largeClosure() {
    return largeObj;
  };
  return lc;
}
const memo = new WeakMap();
function memoize(obj) {
  if (memo.has(obj)) {
    console.log('Get cached result');
    return memo.get(obj);
  }
  const compute = obj.a + obj.b;
  console.log('Set computed result to caching map');
  memo.set(obj, compute);
  return compute;
}
function start() {
  const lcObj = createLargeClosure();
  // 返回一个函数
  const timer = setInterval(() => {
    memoize(lcObj());
    // 返回大对象作为key
  }, 1000);
  setTimeout(function () {
    clearInterval(timer);
  }, 5001);
  // 清除定时器不再持有对key的引用
}

如果使用 Map 创建 memo,则使用 DevTools 检查的内存分配状态如下:

如果将缓存对象从 Map 更改为 WeakMap,可以看到上图中的对象已被删除,并且随着内存被释放,上面条形图中的蓝色量表略有下降。

4.2 自定义事件

在某些项目中,通常需要自定义事件对象。 有一种方法可以将事件对象混合到实例中,例如 TOAST UI 的代码片段(查看参考资料)。 但是,如果您只是将事件对象实现为单例,WeakMap 可以用于解决内存泄漏。 以下代码注册了一个以 targetObject 为键的事件处理程序并执行它:

class EventEmitter {
  constructor() {
    this.targets = new WeakMap();
  }
  on(targetObject, handlers) {
    if (!this.targets.has(targetObject)) {
      this.targets.set(targetObject, {});
    }
    const targetHandlers = this.targets.get(targetObject);
    Object.keys(handlers).forEach((handlerName) => {
      targetHandlers[handlerName] = targetHandlers[handlerName] || [];
      targetHandlers[handlerName].push(
        handlers[handlerName].bind(targetObject)
      );
    });
  }
  fire(targetObject, handlerName, args) {
    const targetHandlers = this.targets.get(targetObject);
    if (targetHandlers && targetHandlers[handlerName]) {
      targetHandlers[handlerName].forEach((handler) => handler(args));
    }
  }
}

当执行下面的代码时,user 和 handlers 对象的内存会自动被垃圾收集器收集,因为 user 在 WeakMap 中注册为键。

const emitter = new EventEmitter();
function start() {
  const user = {
    name: 'John',
  };
  const handlers = {
    sayHello: function () {
      console.log(`Hello, my name is ${this.name}`);
    },
    sayGoodBye: function () {
      console.log(`Good bye, my name is ${this.name}`);
    },
  };
  emitter.on(user, handlers);
  // 以对象为key
  const timer = setInterval(() => {
    emitter.fire(user, 'sayHello');
    emitter.fire(user, 'sayGoodBye');
  }, 1000);
  // 清除定时器
  setTimeout(function () {
    clearInterval(timer);
  }, 5001);
}

下图展示了用 Map 实现 EventEmitter 时,user 和 handlers 没有被收集的情况。

如果使用 WeakMap,我们可以看到对象已被垃圾收集器收集。

4.3 私有属性

私有属性可以很容易地在 Babel 的转译结果中找到,以下代码使用 # 关键字指定访问修饰符。

class A {
  #privateFieldA = 1;
  #privateFieldB = 'A';
}

下面显示了转译后的实际结果。您可以在 Babel 的 Try it out(链接)中测试此代码。

'use strict';
var _privateFieldA = /*#__PURE__*/ new WeakMap();
var _privateFieldB = /*#__PURE__*/ new WeakMap();
class A {
  constructor() {
    _privateFieldA.set(this, {
      writable: true,
      value: 1,
    });

    _privateFieldB.set(this, {
      writable: true,
      value: 'A',
    });
  }
}

私有字段使用 WeakMap 的原因如下:

  • 在信息隐藏方面,只有当您知道 WeakMap 实例和类 A 的实例时,才可以使用一个值
  • 在防止内存泄漏方面,如果 A 类的实例引用除了 WeakMap 实例外不存在,则自动回收内存

5.总结

综上所述,WeakMap 是一种通过保留弱引用而在内存泄漏管理方面具有优势的数据结构,尽管它不支持像 Map 那样提供枚举的方法。

除了用于 Vue 3 响应式之外,WeakMap 还用于如何在 lodash 中使用 _.memoize 的示例,这表明可以将缓存对象更改为 WeakMap。它还用于填充私有访问修饰符以隐藏信息。

诚然,现在大部分项目都是基于框架实现的,一般情况下不会经常用到 WeakMap。但是,如果您需要一个以项目中的对象作为标识符的数据结构,您可能需要考虑在使用常规对象或 Map 之前使用 Wea??kMap。

参考资料

https://toastui.medium.com/lets-find-out-about-weakmap-2150905935d1

https://youtu.be/cAIo4dEEPuc?t=1667 https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots/

https://exploringjs.com/es6/ch_classes.html#sec_private-data-via-weakmaps

https://babeljs.io/repl

https://github.com/nhn/tui.code-snippet/blob/master/customEvents/customEvents.js

Tags:

标签列表
最新留言