第一次搞 polyfill,感叹 JavaScript 的 GC 真麻烦


GitHub Gist: https://gist.github.com/CNSeniorious000/9fc1a72e45358dd7c9e2f16e5d26df5c


起因是要给pyodide.http支持在 cancelled 的时候自动 abort,就需要 AbortSignal.any,结果 maintainers 之一发现这个 API 很新,甚至 FireFox 2024年才刚实现。所以叫我得 polyfill

研究了半天发现这还真不容易,而且似乎没有现成的可供 copy,只好自己实现

为此接触了 JavaScript 的WeakRefFinalizationRegistry,还锻炼了一下写 JsDoc 的技能


想法:

  • JavaScript 的FinalizationRegistry相比 python 的__del__还是有不方便的地方的,因为一个 registery 必须自己不被 gc 才能有用
  • JavaScript 里不好手动触发 gc,搞得调试比较玄学
  • JavaScript 里似乎即使 gc 了内存还是不会立即释放,python 里倒似乎能在任务管理器看到内存降下去

下面附上 gist 的内容:

export function polyfillAbortSignalAny() {
  /** @param {AbortSignal[]} signals */
  return (signals) => {
    // if (AbortSignal.any) {
    //   return AbortSignal.any(signals);
    // }
    const controller = new AbortController();
    const controllerRef = new WeakRef(controller);
    /** @type {[WeakRef<AbortSignal>, (() => void)][]} */
    const eventListenerPairs = [];
    let followingCount = signals.length;

    /** @type {FinalizationRegistry<(callback: () => any) => void>} */
    const registry = (globalThis.__abortSignalCleanups =
      globalThis.__abortSignalCleanups ??
      new FinalizationRegistry((callback) => void callback()));

    signals.forEach((signal) => {
      const signalRef = new WeakRef(signal);
      function abort() {
        controllerRef.deref()?.abort(signalRef.deref()?.reason);
      }
      signal.addEventListener("abort", abort);
      eventListenerPairs.push([signalRef, abort]);
      registry.register(signal, () => !--followingCount && clear(), signal);
    });
    function clear() {
      eventListenerPairs.forEach(([signalRef, abort]) => {
        const signal = signalRef.deref();
        if (signal) {
          signal.removeEventListener("abort", abort);
          registry.unregister(signal);
        }
        const controller = controllerRef.deref();
        if (controller) {
          registry.unregister(controller.signal);
          delete controller.signal.__controller;
        }
        console.log("clear", ++count);
      });
    }

    const { signal } = controller;

    registry.register(signal, clear, signal);
    signal.addEventListener("abort", clear);

    registry.register(controller, () => console.log("controller", count));

    signal.__controller = controller;

    return signal;
  };
}

export let count = 0; // for test only

// node --expose-gc

({ polyfillAbortSignalAny } = await import("./polyfill.js"));

any = polyfillAbortSignalAny();

a = Array.from({ length: 2 }).map(() => { const c = new AbortController(); return [c, any([c.signal])]; })

[x, y] = a[0]

x.abort()

// a = a.map(([x, y]) => x) // to remove the references for followers
// a = a.map(([x, y]) => y) // to remove the references for origins

gc()

// look at the output

好了现在更多疑点了。不知道为什么在 vitest 或者以脚本形式运行的话,就是不会触发 gc

所以总是得用命令行打

一开始是用 vitest 发现不行,又用 watchfiles 发现也不行

不过这也算是积累了小型 JavaScript 脚本的调试经验:watchfiles 比 vitest 甚至还方便

但是我 tsconfig 还是配置不对 好烦 而且这玩意算是搞了今天一天

第一次搞 polyfill,感叹 JavaScript 的 GC 真麻烦
编辑此页