核心做法

1var processed = new WeakMap();
2
3shell.querySelectorAll('.pagefind-ui__result').forEach(function (el) {
4  if (processed.has(el)) return;
5  // ... 處理
6  processed.set(el, true);
7});

把「已處理」狀態紀錄在 JS 的 WeakMap 裡、不寫到 DOM 上。WeakMap key 是元素本身、元素被 GC 時自動清理。


這個做法存在的價值

兩件事 DOM attribute 標記 做不到:

  1. 不污染 DOM:使用者 DOM 不會被加自家 attribute、適合第三方 library
  2. 跟 framework 完全解耦:framework 怎麼操作 DOM 都不影響 WeakMap 紀錄

代價是 debug 不便(看不到狀態)、紀錄跟 JS context 綁定(換頁就消失)。


適合的情境

情境為什麼合理
寫第三方 library / npm package不在使用者 DOM 加 attribute、避免命名衝突
Framework 會清非預期的 attributeWeakMap 不在 DOM、framework 動不到
需要週期性 reset 紀錄processed = new WeakMap() 一行重置全部
紀錄複雜資料、不只是 booleanWeakMap value 可以是任何物件

核心特徵:紀錄獨立於 DOM 之外、跟 JS 物件 lifetime 綁定。


不適合的情境

情境為什麼不夠改用
自家 application、devtools debug 重要看不到狀態、debug 困難DOM attribute 標記
跨頁面 / 跨 session 的 idempotencyWeakMap 在 JS context 內、換頁就消失LocalStorage / 後端紀錄
元素生命週期短、頻繁 GCWeakMap 自動清理可能比預期早改用 Map(但要手動清理)
紀錄要跟 SSR 同步WeakMap 只活在 client結合 attribute(SSR 階段標記)

設計細節

為什麼用 WeakMap 不用 Map / Set

1// WeakMap:key 是元素、元素被 GC 時 entry 自動消失
2var processedW = new WeakMap();
3processedW.set(el, true);
4// el 從 DOM 移除 + 沒其他 reference → GC → WeakMap entry 消失
5
6// Map / Set:強引用、阻止 GC
7var processedS = new Set();
8processedS.add(el);
9// el 從 DOM 移除、但 Set 還抓著 → 永久 leak

DOM 元素可能動態移除(filter、SPA 路由切換、framework 重繪)— Map / Set 會造成 memory leak。處理 DOM 元素 idempotency 預設用 WeakMap

Value 的設計

 1// 用法 1:純 boolean(最簡)
 2processed.set(el, true);
 3
 4// 用法 2:紀錄處理版本(升級時偵測 stale 紀錄)
 5processed.set(el, { version: 2, time: Date.now() });
 6if (processed.has(el) && processed.get(el).version === currentVersion) return;
 7
 8// 用法 3:紀錄相關 metadata(避免重複查詢)
 9processed.set(el, {
10  bindingsId: registerListener(el),
11  initialClass: el.className,
12});

WeakMap value 可以儲任何資料 — 比 attribute(只能存字串)更彈性。

Debug 替代方案

attribute 標記可以在 devtools inspector 直接看;WeakMap 看不到。debug 時的替代:

1// 開發模式同步寫一份 attribute(production build 時拿掉)
2function markProcessed(el) {
3  processed.set(el, true);
4  if (DEV_MODE) {
5    el.setAttribute('data-debug-processed', 'true');
6  }
7}

或暴露到 console:

1window.__debug_processed = processed;
2// console: __debug_processed.has($0)  // 檢查當前選中元素

這些都是 workaround、不如 attribute 標記直觀。選 WeakMap 的人通常已經接受這個 debug 成本

Reset 紀錄

1// WeakMap 整批 reset
2processed = new WeakMap();
3
4// 對比 attribute 整批 reset 要遍歷
5shell.querySelectorAll('[data-scoped]').forEach(el => {
6  el.removeAttribute('data-scoped');
7});

需要週期性 reset(例如 user 切換 mode、所有元素該重新處理)— WeakMap 一行解決、attribute 要遍歷。


跟其他 idempotency 做法的關係

#14 Selector 精準度 的「過濾」維度有三種做法:

做法比較
DOM attribute 標記production 預設、devtools 可見、有命名衝突風險
本卡片:WeakMap 紀錄不污染 DOM、適合 library、debug 不便
依賴外部呼叫者保證反模式、無防護

選擇順序:自家 application → attribute;library / framework 衝突 → WeakMap;反模式不選


應用範例:library 設計

 1// 第三方 library export 的 init 函式
 2function initSearchEnhancement(shell) {
 3  var processed = new WeakMap();
 4
 5  function apply() {
 6    shell.querySelectorAll('.search-result').forEach(function (el) {
 7      if (processed.has(el)) return;
 8      enhanceResult(el);
 9      processed.set(el, true);
10    });
11  }
12
13  apply();
14  new MutationObserver(apply).observe(shell, { childList: true, subtree: true });
15}
16
17// 使用者:
18initSearchEnhancement(document.querySelector('.my-search'));
19// 不會在使用者 DOM 上加任何 data-* attribute

使用者 DOM 完全乾淨、library 行為內聚。


應用範例:版本化處理

 1var processed = new WeakMap();
 2var CURRENT_VERSION = 3;
 3
 4function apply() {
 5  shell.querySelectorAll('.x').forEach(function (el) {
 6    var record = processed.get(el);
 7    if (record && record.version === CURRENT_VERSION) return;
 8
 9    // 升級到新版本(可能需要清舊綁定)
10    if (record) cleanup(el, record);
11    enhance(el, CURRENT_VERSION);
12    processed.set(el, { version: CURRENT_VERSION, time: Date.now() });
13  });
14}

版本變動時 — 不需要遍歷 DOM 清舊 attribute、直接用 WeakMap value 比對。


判讀徵兆

訊號該套用本 pattern 嗎?
寫第三方 library / npm package是 — 不污染使用者 DOM
Framework 會 strict 清自家 attribute是 — WeakMap 跟 framework 解耦
紀錄需要儲複雜資料(不只 boolean)是 — WeakMap value 可任意
自家 application、debug 重要否 — attribute 標記 在 inspector 可見
紀錄要跨頁面持久化否 — 改用 storage / 後端

核心原則:WeakMap idempotency 是 attribute 標記的「不污染 DOM 替代品」 — 在 library / framework 衝突情境必要、在自家 application 通常用 attribute 即可。GC 自動清理是 WeakMap 的特性、預設不用 Map / Set 是因為它們會 memory leak。