性能优化之---WeakMap

350 阅读8分钟

随着 JavaScript 应用变得越来越复杂,性能优化也变得至关重要。接下来我们一起深入探讨一下性能优化的技巧之----WeakMap

什么是WeakMap

WeakMap 是 JavaScript 的一个内置对象,用于存储键值对,其键是对象,值可以是任意类型。与普通的 Map 不同,WeakMap 的键是弱引用,这意味着如果没有其他引用指向键对象,这些对象会自动被垃圾回收机制回收。(垃圾回收机制:浏览器的自动内存管理机制,会自动回收不需要用的内存,使其重新可用。以此来保证内存的充足)

注意:WeakMap的键必须是对象

语法

new WeakMap([iterable])
  • iterable:是一个数组(二元数组)或者其他可迭代的且其元素是键值对的对象。每个键值都会被添加到新的 WeakMap 中。如果 iterable 为 null,会被当作 undefined。

属性

  • length:属性的值为 0;
  • prototype:WeakMap 构造函数的原型对象,允许向所有 WeakMap 实例添加属性。

WeakMap 方法

  • set(key, value): 设置键值对,返回这个 WeakMap 对象。。
  • get(key): 获取键对应的值,如果key不存在返回undefined。
  • has(key): 检查是否存在某个键,返回一个布尔值。
  • delete(key): 删除某个键值对。

方法使用示例

创建

创建 WeakMap

const weakMap = new WeakMap();

当然我们也可以在创建时传入一个包含键值对的数组来初始化 WeakMap

const key1 = {};
const key2 = {};

const weakMap = new WeakMap([
    [key1, 'value1'],
    [key2, 'value2']
]);

添加

向 WeakMap 中添加或更新键值对,它会返回 WeakMap 本身,以便可以进行链式调用。

const obj = {};
const returnValue = weakMap.set(obj, 'some value');

console.log(returnValue); // 输出: WeakMap { <items unknown> }

获取

get(key)返回与指定键关联的值,如果该键不存在,则返回 undefined

console.log(weakMap.get(obj)); // 输出 'some value'

console.log(weakMap.get(a));  // 输出: undefined

判断是否存在

has(key)返回一个布尔值,表示 WeakMap 中是否存在指定的键。

console.log(weakMap.has(obj)); // 输出 true

删除

delete(key)移除 WeakMap 中指定的键值对,它返回一个布尔值,表示是否成功删除。

console.log(weakMap.delete(obj)); // 输出: true
console.log(weakMap.has(obj)); // 输出 false
console.log(weakMap.delete(obj)); // 输出: false

看到这里可能会有很多读者有有一种似曾相识的感觉,没错它是不是和我们所熟知的Map很相像。那它很Map有什么区别呢

Map 和 WeakMap 的区别

特性MapWeakMap
键的类型可以是任何类型只能是对象引用
垃圾回收不会自动清除无引用的键值对无引用的键值对会被自动清除
可枚举性可以枚举键、值和条目不可枚举,无法获取集合的大小
内部实现使用数组存储键值对使用弱引用存储键值对
时间复杂度赋值和搜索操作为 O(n)赋值和搜索操作平均时间复杂度为 O(1)
内存管理可能导致内存泄漏不会导致内存泄漏
方法set(), get(), has(), delete(), clear(), keys(), values(), entries()set(), get(), has(), delete()
用途通用键值对存储与对象关联的元数据或私有数据存储

性能优化举例:

在需要频繁计算耗时操作的场景中,可以利用 WeakMap 缓存计算结果,以避免重复计算,这样我们在使用的时候就可以立马调用。提高性能并且WeakMap不需要我们担心内存泄漏的问题。

const cache = new WeakMap(); // 创建一个 WeakMap 用于缓存对象的计算结果

function computeExpensiveOperation(obj) {
  if (!cache.has(obj)) { // 如果缓存中没有存储当前对象的计算结果
    const result = obj.value * 2; // 假设这是一个耗时的计算,将 obj 的 value 属性乘以 2
    cache.set(obj, result); // 将计算结果存入 WeakMap 中,以 obj 作为键
  }
  return cache.get(obj); // 返回 WeakMap 中存储的 obj 对应的计算结果
}

let myObj1 = { value: 10 }; // 创建一个对象 myObj1,value 属性为 10
let myObj2 = { value: 20 }; // 创建一个对象 myObj2,value 属性为 20

console.log(computeExpensiveOperation(myObj1)); // 第一次调用,计算 myObj1 的计算结果并缓存
console.log(computeExpensiveOperation(myObj1)); // 直接从缓存中获取 myObj1 的计算结果,无需重新计算
console.log(computeExpensiveOperation(myObj2)); // 计算 myObj2 的计算结果并缓存,因为是不同的对象

存储对象的私有数据或元数据

在处理复杂的需求时,我们经常需要为 DOM 元素关联一些自定义的数据或元数据。使用 WeakMap 可以有效地存储和管理这些数据,同时确保不会影响垃圾回收对 DOM 元素的处理。

const elementData = new WeakMap(); // 创建一个 WeakMap 用于存储 DOM 元素和其用户数据的映射关系

function setUserData(element, userData) {
  elementData.set(element, userData); // 将 userData 存储到 WeakMap 中,以 element 作为键
}

function getUserData(element) {
  return elementData.get(element); // 获取 WeakMap 中存储的 element 对应的 userData
}

const divElement = document.createElement('div'); // 创建一个新的 div 元素
setUserData(divElement, { name: 'John', age: 30 }); // 设置 div 元素的用户数据为 { name: 'John', age: 30 }

console.log(getUserData(divElement)); // 输出 { name: 'John', age: 30 },获取并打印 div 元素的用户数据


防止内存泄漏的事件监听器管理

在管理事件监听器时,需要特别注意避免因事件监听器未被正确移除而导致的内存泄漏问题。使用 WeakMap 可以有效地管理事件监听器,确保在元素或程序结束时自动移除监听器,而不需要手动清理。

const eventListeners = new WeakMap(); // 创建一个 WeakMap 用于存储事件监听器

function addEventListener(element, event, listener) {
  if (!eventListeners.has(element)) { // 如果 WeakMap 中没有存储当前元素的事件监听器
    eventListeners.set(element, new Map()); // 在 WeakMap 中为当前元素创建一个新的 Map,用于存储事件和监听器
  }
  const elementEvents = eventListeners.get(element); // 获取当前元素对应的事件和监听器 Map
  if (!elementEvents.has(event)) { // 如果当前事件在 Map 中不存在
    elementEvents.set(event, []); // 在事件 Map 中为当前事件创建一个空数组,用于存储监听器
  }
  elementEvents.get(event).push(listener); // 将监听器添加到事件对应的监听器数组中
  element.addEventListener(event, listener); // 向元素添加事件监听器
}

function removeEventListener(element, event, listener) {
  if (eventListeners.has(element)) { // 如果 WeakMap 中存在当前元素的事件监听器
    const elementEvents = eventListeners.get(element); // 获取当前元素对应的事件和监听器 Map
    if (elementEvents.has(event)) { // 如果当前事件在事件 Map 中存在
      const listeners = elementEvents.get(event); // 获取当前事件对应的监听器数组
      const index = listeners.indexOf(listener); // 查找监听器在数组中的索引
      if (index !== -1) { // 如果找到了监听器
        listeners.splice(index, 1); // 从监听器数组中移除该监听器
        element.removeEventListener(event, listener); // 从元素中移除事件监听器
        if (listeners.length === 0) { // 如果当前事件的监听器数组为空
          elementEvents.delete(event); // 从事件 Map 中删除当前事件
        }
      }
    }
  }
}

const button = document.createElement('button'); // 创建一个新的按钮元素
addEventListener(button, 'click', () => { // 给按钮元素添加点击事件监听器
  console.log('Button clicked!'); // 当按钮被点击时输出信息到控制台
});

// 程序结束或元素移除时,无需手动移除事件监听器,垃圾回收会自动处理

扩展

其实除了WeakMap外,它还有两个很相似的兄弟WeakRefWeakSet,我们这里简单对比一下他们三个之间的差别

当然,可以更详细地对比 WeakRefWeakMapWeakSet。以下是一个更全面的对比表格:

特性WeakRefWeakMapWeakSet
存储类型单个对象键-值对,键必须是对象对象集合
键类型不适用只能是对象只能是对象
值类型对象任意类型不适用
键/值是否被强引用否(弱引用)键强引用,值在键被回收后弱引用否(弱引用)
用途中对象的处理提供真正的弱引用,允许对象在生命周期的任意时刻被回收存储对象键-值对,在键被回收后也可以回收相应的值存储对象集合,在对象不再被引用时可以被回收
垃圾回收特性在对象不再被引用时自动回收当键对象被回收后,值对象也会被回收在对象不再被引用时自动回收
主要用途高级内存管理,避免内存泄漏缓存、对象映射等需要对象键值对的场景存储唯一对象的集合,例如跟踪已处理的对象
API 方法deref()set(), get(), delete(), has()add(), delete(), has()
键是否唯一不适用
值是否唯一
可枚举性
内存管理优势减少内存泄漏在键被垃圾回收后,自动回收其关联的值在对象不再被引用时,自动释放内存
适用版本ECMAScript 2021 (ES12)ECMAScript 2015 (ES6)ECMAScript 2015 (ES6)
兼容性较新,支持情况相对较少已广泛支持已广泛支持
典型使用场景缓存管理、保持对象引用但允许垃圾回收缓存、DOM节点映射、存储私有数据跟踪唯一对象集合、管理对象生命周期
代码示例javascript\nlet weakRef = new WeakRef(obj);\nlet derefObj = weakRef.deref();\njavascript\nlet weakMap = new WeakMap();\nweakMap.set(obj, val);\nlet value = weakMap.get(obj);\njavascript\nlet weakSet = new WeakSet();\nweakSet.add(obj);\nlet hasObj = weakSet.has(obj);\n
内存回收时机在对象没有其他引用时可被回收在键没有其他引用时,其关联的值可被回收在对象没有其他引用时可被回收
弱引用的特性提供真正的弱引用,在对象的生命周期中不影响垃圾回收仅在键被回收后才弱引用值在对象不再被引用时,自动允许回收