核心做法

1shell.querySelectorAll('.pagefind-ui__result:not([data-scoped])').forEach(function (el) {
2  // ... 處理
3  el.setAttribute('data-scoped', 'true');
4});

apply 函式入口用 :not([data-x]) 過濾掉已處理元素、處理完後設 attribute 標記。下次 apply 被觸發時、已處理的元素不會被命中。


這個做法存在的價值

把「保證只處理一次」的責任從呼叫端(要記得只呼叫一次)轉到元素本身(看自己有沒有被處理過)。

apply 函式可能被多個源觸發:

  • 初始化時呼叫
  • MutationObserver 偵測到變動觸發
  • 使用者事件觸發
  • Framework 重繪後重新呼叫

任一個源多呼叫就重複處理 — 無法靠呼叫端紀律避免。Idempotency 標記讓 apply 自己防護。


適合的情境

情境為什麼合理
Production apply 函式、可能被多源觸發標記在元素上、不依賴呼叫紀律
處理動作有副作用(綁 listener、改 class)重複觸發會疊加副作用
元素生命週期跟 attribute 同步(不會被 reset)標記跟著元素走、自然清理
Devtools debug 友善attribute 在 inspector 可見

核心特徵:元素的 attribute 跟著元素 DOM 生命週期、元素移除時標記自動消失。


不適合的情境

情境為什麼不夠改用
寫第三方 library在使用者 DOM 加自家 attribute、有命名衝突風險WeakMap 紀錄
Framework 重繪會清掉 attribute標記消失、防護失效配合 disconnect/observe 或改 WeakMap
需要週期性 reset 標記attribute 改回需要遍歷所有元素WeakMap 可整批 new WeakMap()
多種獨立的 idempotency 維度DOM 上多 attribute 互相干擾WeakMap 各別管理

設計細節

Attribute 命名規範

1// 好:明確 namespace + 用途
2el.setAttribute('data-search-scoped', 'true');
3el.setAttribute('data-myapp-processed', 'true');
4
5// 較差:通用名、容易跟其他程式撞
6el.setAttribute('data-processed', 'true');
7el.setAttribute('processed', 'true');  // 不是 data-* 開頭、可能不被 HTML spec 接受

預設用 data-{appname}-{purpose} 格式 — 即使引入第三方 library 加 attribute、也不會撞名。

Attribute 值的選擇

 1// 用法 1:固定 'true'(最簡)
 2el.setAttribute('data-scoped', 'true');
 3
 4// 用法 2:紀錄處理時間 / 版本(debug 友善)
 5el.setAttribute('data-scoped', String(Date.now()));
 6el.setAttribute('data-scoped', 'v2');
 7
 8// 用法 3:boolean attribute(無值)
 9el.setAttribute('data-scoped', '');
10// CSS 用 [data-scoped] 即可選中

預設用 'true'、debug 困難時改 timestamp 看處理順序。

跟 framework 重繪共處

Svelte / React / Vue 重繪元素時、自家 attribute 通常會被保留(framework 只管自己的 attribute)— 但有例外:

情境行為
Framework re-render 整段 DOM元素被替換、新元素沒標記 → apply 重跑、合理
Framework patch 既有元素 attribute自家 attribute 保留
Framework replaceWith / innerHTML 重設元素被替換 → 標記消失、apply 重跑、合理

核心觀察:自家 attribute 跟著元素走 — 元素還在就有、元素被換就沒。這是「正確」行為、不是 bug。

例外:framework 主動清自家 attribute

少數 framework 會 strict 清非預期的 attribute(例如某些 Web Component lib)。檢查方式:

1el.setAttribute('data-scoped', 'true');
2// ... 等 framework patch 一次後
3console.log(el.getAttribute('data-scoped'));  // 還在嗎?

如果消失、改用 WeakMap 紀錄


跟其他 idempotency 做法的關係

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

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

預設用本卡片、第三方 library / framework 衝突情境升級到 WeakMap。


應用範例:完整 apply

 1function apply(shell) {
 2  var newResults = shell.querySelectorAll(
 3    '.pagefind-ui__result:not([data-search-scoped])'
 4  );
 5
 6  newResults.forEach(function (el) {
 7    bindClickHandler(el);
 8    addCustomBadge(el);
 9    el.setAttribute('data-search-scoped', 'true');
10  });
11}
12
13// 多源觸發都安全
14init.addEventListener('click', () => apply(shell));
15observer.observe(shell, ...);  // 觀察到變動觸發 apply
16apply(shell);  // 初始化時跑一次

三個觸發點任一個多跑、:not([data-search-scoped]) 都會過濾掉已處理元素。


應用範例:多維度標記

 1// 三個獨立 idempotency 維度、各自 attribute
 2el.setAttribute('data-search-scoped', 'true');     // scope filter 處理過
 3el.setAttribute('data-search-bound', 'true');      // event listener 綁過
 4el.setAttribute('data-search-decorated', 'true');  // 視覺裝飾加過
 5
 6// 各 apply 函式只看自己的 attribute
 7function applyScope(shell) {
 8  shell.querySelectorAll('.x:not([data-search-scoped])').forEach(...)
 9}
10function applyBindings(shell) {
11  shell.querySelectorAll('.x:not([data-search-bound])').forEach(...)
12}

每個 idempotency 維度獨立 — 互相不干擾。


判讀徵兆

訊號該套用本 pattern 嗎?
Apply 被多源觸發、產生重複處理 bug是 — 直接對應使用情境
寫第三方 library / 不能污染 DOM否 — 改 WeakMap
Framework 會清自家 attribute否 — 改 WeakMap
想在 devtools inspector 直接看處理狀態是 — attribute 可見性是優點
同元素多種 idempotency 維度是 — 多 attribute 各自管理

核心原則:把 idempotency 責任從呼叫端搬到元素本身、attribute 是「便宜可見的旗標」。Production apply 預設用本 pattern、特殊情境(library / framework 衝突)才升級到 WeakMap。