Kodokon kodokon.com

内存与性能:GC、WeakMap/WeakRef、防抖/节流

理解垃圾回收器的工作原理,使用弱引用来避免泄漏,并用防抖和节流来平滑开销较大的工作。

11 分钟 · 3 题

在 Kodokon 中打开本课

V8 的垃圾回收器基于可达性(reachability):一旦没有任何引用链把某个对象连接到根(栈、活动的作用域、全局变量),它就变得可被回收。GC 是分代的:年轻对象生活在*新生代(nursery)*中,会被频繁而快速地回收(scavenge);幸存者会被晋升到老生代,由增量式的标记-清除(mark-and-sweep)处理。经典的泄漏并不来自 GC,而来自你忘记的引用:基于 Map 的缓存、从不解绑的监听器、捕获了大对象的闭包。

JAVASCRIPT
const strongCache = new Map();
const weakCache = new WeakMap();

let session = { user: "ada" };
strongCache.set(session, "data");
weakCache.set(session, "data");

session = null;
// Map: the key retains the object -> memory leak
// WeakMap: the entry becomes collectable
同样的代码,两种截然相反的内存命运。

WeakMap 不会保留它的键:当一个键从其他任何地方都变得不可达时,整个条目就会消失。由此带来的设计后果:键必须是对象(或未注册的 symbol),而且这种结构既不可迭代也不可测量——暴露它的 size 会泄露 GC 的行为,而 GC 是非确定性的。WeakRef 更进一步:它给你一个直接的弱引用,需要用 deref() 来解引用。

JAVASCRIPT
let config = { theme: "dark" };
const ref = new WeakRef(config);

function readTheme() {
  const target = ref.deref();
  return target ? target.theme : "default";
}

console.log(readTheme()); // "dark"
config = null;
// After a GC pass, deref() may
// return undefined: plan a fallback.
deref() 可能返回该对象……也可能返回 undefined。

在感知性能一侧,高频事件(scrollresizeinputmousemove)每秒可能触发数百次调用。有两种互补的策略:防抖(debounce)只在一段静默期之后才运行函数(非常适合输入即搜索),而节流(throttle)则保证每个时间间隔内至多执行一次(非常适合跟踪滚动)。

JAVASCRIPT
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(
      () => fn.apply(this, args),
      delay
    );
  };
}
防抖:一连串调用中只有最后一次算数。
JAVASCRIPT
function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {
      last = now;
      fn.apply(this, args);
    }
  };
}
节流:每个间隔内至多执行一次。

知识检测

确认你已牢记本课的重点内容。

  1. 为什么 WeakMap 的键必须是对象,而不能是像字符串这样的原始值?
    • 出于哈希性能的原因
    • 因为只有对对象的引用才能变得不可达,从而触发条目的移除
    • 这是一个自 ES2021 起就已解除的历史限制
    • 因为在 JavaScript 中原始值不能用作键
  2. 防抖和节流的根本区别是什么?
    • 防抖在一段静默期之后运行,节流保证每个间隔内至多执行一次
    • 防抖是异步的,节流是同步的
    • 节流会取消之前的调用,防抖会把它们排队
    • 它们是同一种技术的两个名字
  3. 在一个 WeakRef 上,ref.deref() 可能返回什么?
    • 始终返回原始对象,只要该 WeakRef 存在
    • 如果对象仍然存活就返回该对象,如果它已被回收就返回 undefined
    • 该对象的一份深拷贝
    • 如果对象已被回收就返回 null