核心做法

1document.addEventListener('click', function (e) {
2  var shell = e.target.closest('.search-shell');
3  if (!shell) return;
4  // 在這個 shell 內處理
5  handleSearchClick(shell, e);
6});

不在初始化時綁定 listener、而是頁面層級委派事件、事件處理時從 e.target 反向找元件根。


這個做法存在的價值

把「找元件根」從「初始化時綁定」延後到「事件發生時動態判斷」 — 換到三個能力:

  1. 元件動態增減免處理:新加的元件不需要重新綁 listener
  2. 多實例不需要 forEach setup:所有實例共用一個 listener
  3. 記憶體效率:N 個元件只綁 1 個 listener、不是 N 個

代價是事件處理邏輯多一層(每次都要 closest 反向找)。


適合的情境

情境為什麼合理
SPA 路由切換、元件動態 mount/unmount不需要在 mount 時重綁 listener
元件數量大(>10 個實例)事件委派比每實例綁 listener 省記憶體
元件透過 AJAX 動態注入注入後不需要任何 setup 動作
第三方 widget、不能控制元件生命週期listener 綁在 document、跟 widget 解耦

核心特徵:元件的 mount 時機 / 數量 runtime 才知道、不是初始化時固定。


不適合的情境

情境為什麼過度工程改用
元件靜態 mount、生命週期跟頁面一樣委派多一層、收益不明顯起點當參數
一個元件實例、永不變動完全沒必要元件根變數
需要在元件 mount 時就跑邏輯(不只回應事件)closest 只在事件發生時跑、無法當 init hook起點當參數 + MutationObserver

設計細節

Closest 失敗的處理

1document.addEventListener('click', function (e) {
2  var shell = e.target.closest('.search-shell');
3  if (!shell) return;  // 點擊不在任何 shell 內
4  // ...
5});

closest 找不到時回 null、提早 return 是必要防護。沒這個 check 會在頁面其他地方點擊時報錯

從 closest 結果再往下 query

1var shell = e.target.closest('.search-shell');
2var input = shell.querySelector('.pagefind-ui__search-input');

closest 找到 shell 後、可以從 shell 往下 query 同元件內的其他元素 — 這是「事件 + closest + 局部 query」的組合。

事件類型的選擇

事件適合
click點擊互動
input輸入框文字變動(需要 bubble)
change選項變動(select / radio / checkbox)
keydown鍵盤快捷鍵
focus / blur焦點移動(不 bubble、要用 focusin / focusout

注意 focus / blur 不會 bubble — 事件委派要用 focusin / focusout

委派的根節點選擇

1// 選項 1:document(最寬)
2document.addEventListener('click', handler);
3
4// 選項 2:特定容器(縮範圍)
5var pageContainer = document.querySelector('main');
6pageContainer.addEventListener('click', handler);

縮範圍的好處是「跟其他頁面區域的 listener 不互相干擾」。預設用 document、有干擾風險才縮。


跟其他起點做法的關係

#14 Selector 精準度 的「起點」維度有四種做法:

做法比較
document query靜態、簡潔、無多實例支援
元件根變數靜態、shell 唯一假設
起點當參數靜態多實例、forEach 一次設定
本卡片:closest 反向找根動態、事件驅動、無 init 時機綁定

複雜度遞增、能處理的動態程度也遞增。最動態的場景才用本 pattern。


應用範例:跨多 shell 的 scope filter

 1function setupGlobalScopeFilter() {
 2  document.addEventListener('change', function (e) {
 3    var shell = e.target.closest('.search-shell');
 4    if (!shell) return;
 5
 6    var scope = e.target.closest('.search-scope');
 7    if (!scope) return;  // 不是 scope 控制的 change
 8
 9    applyScope(shell, scope);
10  });
11}
12setupGlobalScopeFilter();

一個 listener 處理所有 shell 的 scope 變動 — 不論 shell 是初始 mount 的、還是 runtime 注入的。


應用範例:與 起點當參數 組合

 1// 初始化階段:對已存在的 shell 做 setup
 2document.querySelectorAll('.search-shell').forEach(setupSearchShell);
 3
 4// 事件階段:用 closest 處理可能新加的 shell
 5document.addEventListener('click', function (e) {
 6  var shell = e.target.closest('.search-shell');
 7  if (!shell) return;
 8  // 處理事件、不論 shell 是初始的還是後加的
 9});
10
11// MutationObserver:捕捉新加的 shell 做 setup
12new MutationObserver(function (mutations) {
13  mutations.forEach(function (m) {
14    m.addedNodes.forEach(function (node) {
15      if (node.matches && node.matches('.search-shell')) {
16        setupSearchShell(node);
17      }
18    });
19  });
20}).observe(document.body, { childList: true, subtree: true });

三個 pattern 組合:「靜態 setup」+「事件動態」+「mount 時 setup」 — 各 pattern 補不同時間點的需求。


判讀徵兆

訊號該套用本 pattern 嗎?
元件 SPA 路由動態切換是 — 直接對應使用情境
元件數量大、每實例都要綁 listener是 — 委派省記憶體
AJAX / Web Component runtime 注入是 — 不需要重綁
確定元件靜態、生命週期固定否 — 起點當參數 已夠
邏輯不是事件驅動(init 時就要跑)否 — closest 只在事件發生時跑

核心原則:closest 反向找根把「定位元件」從綁定時延後到事件發生時 — 換到動態能力、付出的是事件處理多一層判斷。靜態場景用更簡單的做法、動態場景才升級到本 pattern。