终于在 JS 中用上 WeakMap 了!

297次阅读  |  发布于3年以前

当我在处理一个滑动组件时,遇到了一个问题,当我快速切换元素的打开和关闭状态时,如果不允许上一个动画完成,新动画最终会失控,阻断后面的动画效果。

问题原因

因为每次触发动画时,我都会获取元素的当前“原始”高度,无论它是不是在渲染动画,这个库使用的是 Web Animations API,参考下面的代码:

// For each trigger, animate between zero and the `clientHeight` of the element.
let frames = ["0px", `${element.clientHeight}px`].map((height) => {
  return { height, overflow: "hidden" };
});

为了解决这个问题,我需要在滑动组件第一次使用时计算并缓存一次展开的高度,然后在每次触发动画时引用这个缓存。这样,每个页面加载时都会有一个固定的扩展高度值来进行动画的移动,并且不会再因为快速点击而引起这样怪异的现象。

几个选择

很快我想到了几个可能的解决方案。

首先,将这个值存储在目标元素的属性中:这本来是可以实现的,但是不太优雅,当我们审查页面元素时,不希望看到一堆乱七八糟的属性,特别是其他的库可能也需要他们自己的属性,累加起来这些标签的属性可能会变得非常负载,于是我选择弃用这个方法。

另外就是在 window 增加一个缓存对象。但是一个页面上可能同时有多个滑动组件。所以一个单独的 window.seCache 变量不能满足我们的需求。我们需要的是拥有某种键值对的对象。我可以在其中存储对每个元素的引用和相应的扩展高度值。

但它有一个 key 的限制:普通的对象是不允许使用 HTML 节点作为属性的,因此我还需要要求每个元素上都存在一个唯一标识符,作为 key 使用,所以这个方法也不是那么好。

使用 DOM 节点作为 key

这时,有一个朋友给我贴了段代码,使用的是 ES6 的 Computed property names,我大吃一惊:

<span id="el1">first element</span>
<span id="el2">second element</span>

<script>
  const someObj = {
    [document.getElementById('el1')]: 'some value'
  };

  console.log(someObj[document.getElementById('el1')]);
  // 'some value'
</script>

确实,通过 DOM 访问这个值确实会返回所需的值。但是,在深入研究之后,我意识到它并不是根据对该对象的引用执行查找的。相反,它是将其转换为该对象的字符串表示形式,然后将其用作 key:

console.log(Object.keys(someObj));
// ['object HTMLSpanElement']

所以以下任何一项也将访问到相同的值:

console.log(someObj[document.getElementById('el2')]);
// 'some value'

console.log(someObj[document.createElement('span')]);
// 'some value'

这时另一种选择就来了:一组新的原生 JavaScript 对象,允许你使用对象作为键 —— 包括对 DOM 节点本身的引用。也就是 MapWeakMap 对象。例如:

<span id="thing" class="thing">a thing.</span>

<script>
const myWeakMap = new WeakMap();

// Set a value to a specific node reference.
myWeakMap.set(document.getElementById('thing'), 'some value');

// Access that value by passing the same reference.
console.log(myWeakMap.get(document.querySelector('.thing')); // 'some value'
</script>

标准的 Map 是可以解决问题的,但是为啥在这里使用 WeakMap 呢。

WeakMapMap 的主要区别就是:WeakMap 的键名所引用的对象是弱引用。

弱引用:在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

JavaScript 中,一般我们创建一个对象,都是建立一个强引用:

var obj = new Object();

只有当我们手动设置 obj = null 的时候,才有可能回收 obj 所引用的对象。

而如果我们能创建一个弱引用的对象:

var obj = new WeakObject();

我们什么都不用做,只用静静的等待垃圾回收机制执行,obj 所引用的对象就会被回收。

所以,现在这个场景我们使用 WeakMap 再合适不过了, WeakMap 使用的所有的 key 都会在合适的场景下被回收,我们就不用担心内存泄漏了~

下面再来看看我们的代码:

window.seCache = window.seCache || WeakMap.new();

function getExpandedHeight() {
  // We already have the calculated height.
  if(window.seCache.get(element)) {
    return window.seCache.get(element);
  }

  // This is the first run. Calculate & cache the full height.
  element.style.display = "block";
  window.seCache.set(element, element.clientHeight);
  element.style.display = "none";

  return window.seCache.get(element);
}

// For each trigger, animate between zero and the `clientHeight` of the element.
let frames = ["0px", `${getExpandedHeight()}px`].map((height) => {
  return { height, overflow: "hidden" };
});

至此,曾经只在面试题里出现的 WeakMap 终于派上用场了~

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8