<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>DOM on Tarragon</title><link>https://tarrragon.github.io/blog/tags/dom/</link><description>Recent content in DOM on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sun, 26 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/dom/index.xml" rel="self" type="application/rss+xml"/><item><title>拓樸理解先行於 CSS 規則</title><link>https://tarrragon.github.io/blog/report/dom-topology-before-css/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/dom-topology-before-css/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>CSS 是基於 DOM tree 的規則系統 — 不知道 tree 的真實結構，寫的 CSS 規則無法生效。&lt;/strong> 看 class name 的命名規則（如 &lt;code>__form&lt;/code>、&lt;code>__drawer&lt;/code> 看起來像 sibling）容易推錯層級；寫 CSS 之前用工具直接讀 live DOM tree、確認哪些是 grid item、哪些是 grid item 內部的子元素。&lt;/p>
&lt;hr>
&lt;h2 id="層級必須從-live-dom-讀">層級必須從 live DOM 讀&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>CSS class name 是「用途標記」、不是「結構描述」。&lt;code>.parent__child&lt;/code> 這種 BEM 風格在很多框架裡只是作者方便辨認用途，跟元素之間的 DOM parent-child 關係無對應。&lt;/p>
&lt;p>當作者在 wrapper 裡又加一層 wrapper，class name 不一定改 — 同一個 class name 在不同框架版本可能對應不同的 DOM 巢狀。&lt;/p>
&lt;p>唯一能確定 DOM 層級的方法是&lt;strong>讀 live DOM&lt;/strong>。&lt;/p>
&lt;h3 id="看-dom-的工具選擇">看 DOM 的工具選擇&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;th>限制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>瀏覽器 DevTools Elements 面板&lt;/td>
 &lt;td>手動探索、單次確認&lt;/td>
 &lt;td>截圖溝通慢、不能寫成測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>playwright browser_evaluate&lt;/code>&lt;/td>
 &lt;td>程式化讀 parent chain、computed style、bounding rect&lt;/td>
 &lt;td>需要 server 跑著&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>框架原始碼（svelte template、JSX）&lt;/td>
 &lt;td>確認靜態 DOM 結構&lt;/td>
 &lt;td>動態渲染情境看不到&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>優先用 playwright — 同一段 query 可以重複跑、結果可以寫進測試。&lt;/p>
&lt;hr>
&lt;h2 id="這次任務的拓樸誤判">這次任務的拓樸誤判&lt;/h2>
&lt;h3 id="觀察">觀察&lt;/h3>
&lt;p>要把 search scope UI 放在「搜尋輸入框與結果之間」。基於 class name 推測 DOM 結構：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">.pagefind-ui
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">├── .pagefind-ui__form ← 搜尋輸入框
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">└── .pagefind-ui__drawer ← 結果（與 filter）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Class name &lt;code>__form&lt;/code> 與 &lt;code>__drawer&lt;/code> 都用 &lt;code>__&lt;/code> 前綴、並列在 &lt;code>.pagefind-ui&lt;/code> 下、看起來是 sibling。&lt;/p>
&lt;h3 id="判讀">判讀&lt;/h3>
&lt;p>依此假設寫 CSS Grid：把 &lt;code>.pagefind-ui&lt;/code> 設為 grid、用 &lt;code>display: contents&lt;/code> 串接、把 form 放 row 2、scope 放 row 3、drawer 放 row 4。&lt;/p>
&lt;p>實際渲染後：scope 跑到頁尾。&lt;/p>
&lt;p>用 &lt;code>playwright browser_evaluate&lt;/code> 讀 live DOM tree：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">const&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">let&lt;/span> &lt;span class="nx">parents&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[],&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">parents&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tagName&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39;.&amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">parentElement&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>結果：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">DIV.pagefind-ui__drawer
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">FORM.pagefind-ui__form ← drawer 在 form 內！
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">DIV.pagefind-ui
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">DIV#search&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>drawer 是 form 的 child、不是 sibling。我們的 grid 規則把 form（含 drawer 全部結果）放在 row 2、scope 放 row 3 — scope 自然跑到所有結果之後。&lt;/p>
&lt;h3 id="執行">執行&lt;/h3>
&lt;p>確認 DOM 後改用「scope absolute 浮在 form 上、drawer 用 margin-top 讓位」的策略 — 不再嘗試把 form 與 drawer 拆到不同 grid row。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>CSS 是基於 DOM tree 的規則系統 — 不知道 tree 的真實結構，寫的 CSS 規則無法生效。</strong> 看 class name 的命名規則（如 <code>__form</code>、<code>__drawer</code> 看起來像 sibling）容易推錯層級；寫 CSS 之前用工具直接讀 live DOM tree、確認哪些是 grid item、哪些是 grid item 內部的子元素。</p>
<hr>
<h2 id="層級必須從-live-dom-讀">層級必須從 live DOM 讀</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>CSS class name 是「用途標記」、不是「結構描述」。<code>.parent__child</code> 這種 BEM 風格在很多框架裡只是作者方便辨認用途，跟元素之間的 DOM parent-child 關係無對應。</p>
<p>當作者在 wrapper 裡又加一層 wrapper，class name 不一定改 — 同一個 class name 在不同框架版本可能對應不同的 DOM 巢狀。</p>
<p>唯一能確定 DOM 層級的方法是<strong>讀 live DOM</strong>。</p>
<h3 id="看-dom-的工具選擇">看 DOM 的工具選擇</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>適用情境</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>瀏覽器 DevTools Elements 面板</td>
          <td>手動探索、單次確認</td>
          <td>截圖溝通慢、不能寫成測試</td>
      </tr>
      <tr>
          <td><code>playwright browser_evaluate</code></td>
          <td>程式化讀 parent chain、computed style、bounding rect</td>
          <td>需要 server 跑著</td>
      </tr>
      <tr>
          <td>框架原始碼（svelte template、JSX）</td>
          <td>確認靜態 DOM 結構</td>
          <td>動態渲染情境看不到</td>
      </tr>
  </tbody>
</table>
<p>優先用 playwright — 同一段 query 可以重複跑、結果可以寫進測試。</p>
<hr>
<h2 id="這次任務的拓樸誤判">這次任務的拓樸誤判</h2>
<h3 id="觀察">觀察</h3>
<p>要把 search scope UI 放在「搜尋輸入框與結果之間」。基於 class name 推測 DOM 結構：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">.pagefind-ui
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── .pagefind-ui__form        ← 搜尋輸入框
</span></span><span class="line"><span class="ln">3</span><span class="cl">└── .pagefind-ui__drawer      ← 結果（與 filter）</span></span></code></pre></div><p>Class name <code>__form</code> 與 <code>__drawer</code> 都用 <code>__</code> 前綴、並列在 <code>.pagefind-ui</code> 下、看起來是 sibling。</p>
<h3 id="判讀">判讀</h3>
<p>依此假設寫 CSS Grid：把 <code>.pagefind-ui</code> 設為 grid、用 <code>display: contents</code> 串接、把 form 放 row 2、scope 放 row 3、drawer 放 row 4。</p>
<p>實際渲染後：scope 跑到頁尾。</p>
<p>用 <code>playwright browser_evaluate</code> 讀 live DOM tree：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">let</span> <span class="nx">parents</span> <span class="o">=</span> <span class="p">[],</span> <span class="nx">el</span> <span class="o">=</span> <span class="nx">drawer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="nx">el</span> <span class="o">&amp;&amp;</span> <span class="nx">el</span> <span class="o">!==</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">parents</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">tagName</span> <span class="o">+</span> <span class="s1">&#39;.&#39;</span> <span class="o">+</span> <span class="nx">el</span><span class="p">.</span><span class="nx">className</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">el</span> <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>結果：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">DIV.pagefind-ui__drawer
</span></span><span class="line"><span class="ln">2</span><span class="cl">FORM.pagefind-ui__form        ← drawer 在 form 內！
</span></span><span class="line"><span class="ln">3</span><span class="cl">DIV.pagefind-ui
</span></span><span class="line"><span class="ln">4</span><span class="cl">DIV#search</span></span></code></pre></div><p>drawer 是 form 的 child、不是 sibling。我們的 grid 規則把 form（含 drawer 全部結果）放在 row 2、scope 放 row 3 — scope 自然跑到所有結果之後。</p>
<h3 id="執行">執行</h3>
<p>確認 DOM 後改用「scope absolute 浮在 form 上、drawer 用 margin-top 讓位」的策略 — 不再嘗試把 form 與 drawer 拆到不同 grid row。</p>
<hr>
<h2 id="內在屬性比較拓樸推理的可靠性">內在屬性比較：拓樸推理的可靠性</h2>
<table>
  <thead>
      <tr>
          <th>推理來源</th>
          <th>可靠性</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Live DOM（playwright / DevTools）</td>
          <td>最高 — 反映實際渲染</td>
          <td>Debug、整合外部組件</td>
      </tr>
      <tr>
          <td>框架 source / template</td>
          <td>高 — 靜態結構</td>
          <td>自家組件、可讀的 source</td>
      </tr>
      <tr>
          <td>Class name 命名規則</td>
          <td>低 — 命名是慣例、不是契約</td>
          <td>僅參考、不依賴</td>
      </tr>
      <tr>
          <td>視覺截圖推測</td>
          <td>最低 — 看不到 DOM 包裹層</td>
          <td>不應作為唯一依據</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>Live DOM &gt; source &gt; 命名 &gt; 視覺</strong>。Class name 與視覺只能形成假設、必須用前兩者驗證。</p>
<hr>
<h2 id="display-contents-的拓樸限制">display: contents 的拓樸限制</h2>
<p>當決定用 <code>display: contents</code> 串接讓子元素參與外層 grid，必須注意：<strong>contents 只能讓直接子節點上去、不能跨越多層 box</strong>。</p>
<p>例：要讓 form 內的 drawer 參與 search-shell 的 grid，需要 form 也設 <code>display: contents</code>。但 form 設 contents 後：</p>
<ul>
<li>form 自己的 box 消失</li>
<li>依賴 form 為 offset parent 的子元素（如 absolute 定位的 clear button）失去定位基準</li>
<li>form 的 <code>::before</code> / <code>::after</code> 偽元素可能不渲染</li>
</ul>
<p><strong>display: contents 適用條件</strong>：中間層 box 沒有自己的視覺責任（背景、邊框、定位、尺寸） — 否則拆開後視覺破壞。</p>
<hr>
<h2 id="設計取捨拓樸理解的方法">設計取捨：拓樸理解的方法</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（讀 live DOM）當預設、其他做法在特定情境合理。</p>
<h3 id="a讀-live-domplaywright--devtools這個專案的預設">A：讀 live DOM（playwright / DevTools）（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：用 <code>playwright browser_evaluate</code> 讀 ancestor chain、computed style、bounding rect；或 DevTools Elements 面板手動探索</li>
<li><strong>選 A 的理由</strong>：反映實際渲染結果、跨 framework 都對、可寫成測試</li>
<li><strong>適合</strong>：debug、整合外部組件、寫第一版 CSS 之前</li>
<li><strong>代價</strong>：需要 server 跑著（可用 hugo dev / static server）</li>
</ul>
<h3 id="b讀框架-source--template">B：讀框架 source / template</h3>
<ul>
<li><strong>機制</strong>：直接看 svelte / react component 的 template</li>
<li><strong>跟 A 的取捨</strong>：B 看靜態結構、A 看 runtime 結構；B 對自家組件夠用、對動態渲染（runtime wrapper / portal）會漏</li>
<li><strong>B 比 A 好的情境</strong>：自家組件、template 跟 DOM 1:1 對應、不需要 runtime 確認</li>
</ul>
<h3 id="c用-class-name-命名規則推測">C：用 class name 命名規則推測</h3>
<ul>
<li><strong>機制</strong>：看 <code>.parent__child</code> 推測 DOM 巢狀</li>
<li><strong>跟 A 的取捨</strong>：C 完全不需要工具、A 需要 server；但 C 命名是慣例不是契約、容易錯</li>
<li><strong>C 才合理的情境</strong>：初步假設、必須用 A/B 驗證後才能寫 CSS — 不應作為唯一依據</li>
</ul>
<h3 id="d視覺截圖推測">D：視覺截圖推測</h3>
<ul>
<li><strong>機制</strong>：看截圖猜 DOM 結構</li>
<li><strong>成本特別高的原因</strong>：截圖看不到 wrapper、看不到 display: contents 等不可視結構</li>
<li><strong>D 是反模式</strong>：視覺上看起來相同的 DOM 可能完全不同 — 截圖驗收的盲區會在規則寫了不生效時才被發現、debug 成本指數放大</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>可能的根因</th>
          <th>第一個該嘗試的動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫好的 CSS 規則完全沒生效</td>
          <td>元素根本不在預期的 DOM 位置</td>
          <td>用 playwright <code>browser_evaluate</code> 讀 ancestor chain</td>
      </tr>
      <tr>
          <td>Grid / flex 排序與預期不符</td>
          <td>子元素不是直接 grid item</td>
          <td>確認 grid container 的 direct children</td>
      </tr>
      <tr>
          <td>設了 <code>display: contents</code> 後某些定位元素跑掉</td>
          <td>那層 box 是 absolute 元素的 offset parent</td>
          <td>把該層 box 留下、找其他方式達成 layout</td>
      </tr>
      <tr>
          <td>框架重繪後 layout 完全變了</td>
          <td>框架增加了 wrapper 元素</td>
          <td>重新讀 live DOM、更新 CSS 假設</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：CSS 行為與預期不符 ≥ 1 次，先回去看 DOM tree、不要繼續調 CSS 規則。先看才不會試錯。</p>
]]></content:encoded></item><item><title>JS 操作 framework 元件：邊界辨識與安全規則</title><link>https://tarrragon.github.io/blog/report/component-boundary-and-js-impact/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/component-boundary-and-js-impact/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>JS 操作 framework 元件前先界定邊界、選對應的安全規則執行。&lt;/strong> 邊界 = 契約 = 安全範圍。整節點搬遷安全、改節點內部不安全、改節點 attribute 是灰區。每類操作有對應的安全規則 — 不是「能不能動」、是「動了之後 framework 會不會 revert」。&lt;/p>
&lt;blockquote>
&lt;p>本篇焦點：&lt;strong>framework 元件本身需要動時的安全規則&lt;/strong>。「客製 UI 該放哪」由 &lt;a href="../coexisting-with-framework-managed-dom/">#5 客製 UI 留 framework 邊界外&lt;/a> 處理 — 預設應該完全不動 framework、需要動時才參考本篇。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="為什麼邊界要先界定">為什麼邊界要先界定&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>每個元件（自家或 framework 提供）有「對外契約」與「內部實作」。對外契約包括：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>契約類型&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>DOM identity&lt;/td>
 &lt;td>哪些 class / id / attribute 是穩定的&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>State 來源&lt;/td>
 &lt;td>元件內部 state 由誰寫、何時改&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>渲染週期&lt;/td>
 &lt;td>元件何時重繪、重繪時影響哪些 DOM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對外介面&lt;/td>
 &lt;td>提供哪些 props / events / API hooks&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>JS 操作前不知道這些 = 黑箱操作。動了什麼、會觸發什麼、誰會被影響、不可預測。&lt;/p>
&lt;h3 id="邊界宣告的格式">邊界宣告的格式&lt;/h3>
&lt;p>開始 JS 操作之前、寫一段註解或 mental note：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">動什麼：filter-panel 的 parent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">邊界：filter-panel 整個節點 OK，內部子節點屬於 pagefind 管
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">State：checkbox 勾選狀態存在 panel 子節點上、由 pagefind 維護
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">動作：appendChild 整節點 reparent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">為什麼安全：節點 identity 不變、pagefind 在下次 patch 時看到節點還在&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段宣告把「動什麼」「不能動什麼」「為什麼安全」說清楚 — 不是儀式、是強迫自己想清楚再動。&lt;/p>
&lt;hr>
&lt;h2 id="三類操作的安全度">三類操作的安全度&lt;/h2>
&lt;p>從最安全到最不安全：&lt;/p>
&lt;h3 id="1-整節點-reparent安全">1. 整節點 reparent（安全）&lt;/h3>
&lt;p>把 framework 管的整個節點搬到別處 — 節點 identity 不變、framework 在下次 patch 時仍認得它。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 安全 — 整節點搬位置
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">sidebar&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-改節點內部子節點不安全">2. 改節點內部子節點（不安全）&lt;/h3>
&lt;p>在 framework 管的節點內 appendChild / removeChild / 改子節點屬性 — framework 會 revert。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 不安全 — 在 framework 子樹內加東西
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">myCustomDiv&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">filterPanel&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.x&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-y&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;z&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-改節點自身的-attribute--inline-style灰區">3. 改節點自身的 attribute / inline style（灰區）&lt;/h3>
&lt;p>改 framework 管的節點本身的 attribute、看 framework 是否認為這屬於 reactive：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 灰區 — 看 framework 怎麼處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">style&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">display&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;none&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;my-state&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;aria-hidden&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>下節展開三類各自的設計細節。&lt;/p>
&lt;hr>
&lt;h2 id="整節點-reparent為什麼安全怎麼做才安全">整節點 reparent：為什麼安全、怎麼做才安全&lt;/h2>
&lt;h3 id="為什麼安全">為什麼安全&lt;/h3>
&lt;p>Framework 的 reconciliation 通常以「節點 identity」為依據 — &lt;strong>同一個節點在哪裡不重要、節點存不存在才重要&lt;/strong>。&lt;/p>
&lt;p>把 &lt;code>.pagefind-ui__filter-panel&lt;/code> 從 drawer 移到外部 aside：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Framework 看到&lt;/th>
 &lt;th>反應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>節點還在（identity 沒變）&lt;/td>
 &lt;td>繼續更新它的內部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點的 parent 變了&lt;/td>
 &lt;td>Framework 不關心 — parent 不在 component tree 內&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點內的 children 不變&lt;/td>
 &lt;td>Framework 不需要重建&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>reconciliation 不會因為「位置變了」而重建節點 — 重建只發生在「節點消失了 + 新節點出現」的情境。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>JS 操作 framework 元件前先界定邊界、選對應的安全規則執行。</strong> 邊界 = 契約 = 安全範圍。整節點搬遷安全、改節點內部不安全、改節點 attribute 是灰區。每類操作有對應的安全規則 — 不是「能不能動」、是「動了之後 framework 會不會 revert」。</p>
<blockquote>
<p>本篇焦點：<strong>framework 元件本身需要動時的安全規則</strong>。「客製 UI 該放哪」由 <a href="../coexisting-with-framework-managed-dom/">#5 客製 UI 留 framework 邊界外</a> 處理 — 預設應該完全不動 framework、需要動時才參考本篇。</p></blockquote>
<hr>
<h2 id="為什麼邊界要先界定">為什麼邊界要先界定</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>每個元件（自家或 framework 提供）有「對外契約」與「內部實作」。對外契約包括：</p>
<table>
  <thead>
      <tr>
          <th>契約類型</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DOM identity</td>
          <td>哪些 class / id / attribute 是穩定的</td>
      </tr>
      <tr>
          <td>State 來源</td>
          <td>元件內部 state 由誰寫、何時改</td>
      </tr>
      <tr>
          <td>渲染週期</td>
          <td>元件何時重繪、重繪時影響哪些 DOM</td>
      </tr>
      <tr>
          <td>對外介面</td>
          <td>提供哪些 props / events / API hooks</td>
      </tr>
  </tbody>
</table>
<p>JS 操作前不知道這些 = 黑箱操作。動了什麼、會觸發什麼、誰會被影響、不可預測。</p>
<h3 id="邊界宣告的格式">邊界宣告的格式</h3>
<p>開始 JS 操作之前、寫一段註解或 mental note：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">動什麼：filter-panel 的 parent
</span></span><span class="line"><span class="ln">2</span><span class="cl">邊界：filter-panel 整個節點 OK，內部子節點屬於 pagefind 管
</span></span><span class="line"><span class="ln">3</span><span class="cl">State：checkbox 勾選狀態存在 panel 子節點上、由 pagefind 維護
</span></span><span class="line"><span class="ln">4</span><span class="cl">動作：appendChild 整節點 reparent
</span></span><span class="line"><span class="ln">5</span><span class="cl">為什麼安全：節點 identity 不變、pagefind 在下次 patch 時看到節點還在</span></span></code></pre></div><p>這段宣告把「動什麼」「不能動什麼」「為什麼安全」說清楚 — 不是儀式、是強迫自己想清楚再動。</p>
<hr>
<h2 id="三類操作的安全度">三類操作的安全度</h2>
<p>從最安全到最不安全：</p>
<h3 id="1-整節點-reparent安全">1. 整節點 reparent（安全）</h3>
<p>把 framework 管的整個節點搬到別處 — 節點 identity 不變、framework 在下次 patch 時仍認得它。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 安全 — 整節點搬位置
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">sidebar</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filterPanel</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span></span></span></code></pre></div><h3 id="2-改節點內部子節點不安全">2. 改節點內部子節點（不安全）</h3>
<p>在 framework 管的節點內 appendChild / removeChild / 改子節點屬性 — framework 會 revert。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 不安全 — 在 framework 子樹內加東西
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">filterPanel</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">myCustomDiv</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">filterPanel</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.x&#39;</span><span class="p">).</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-y&#39;</span><span class="p">,</span> <span class="s1">&#39;z&#39;</span><span class="p">);</span></span></span></code></pre></div><h3 id="3-改節點自身的-attribute--inline-style灰區">3. 改節點自身的 attribute / inline style（灰區）</h3>
<p>改 framework 管的節點本身的 attribute、看 framework 是否認為這屬於 reactive：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 灰區 — 看 framework 怎麼處理
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s1">&#39;none&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;my-state&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;aria-hidden&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span></span></span></code></pre></div><p>下節展開三類各自的設計細節。</p>
<hr>
<h2 id="整節點-reparent為什麼安全怎麼做才安全">整節點 reparent：為什麼安全、怎麼做才安全</h2>
<h3 id="為什麼安全">為什麼安全</h3>
<p>Framework 的 reconciliation 通常以「節點 identity」為依據 — <strong>同一個節點在哪裡不重要、節點存不存在才重要</strong>。</p>
<p>把 <code>.pagefind-ui__filter-panel</code> 從 drawer 移到外部 aside：</p>
<table>
  <thead>
      <tr>
          <th>Framework 看到</th>
          <th>反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>節點還在（identity 沒變）</td>
          <td>繼續更新它的內部</td>
      </tr>
      <tr>
          <td>節點的 parent 變了</td>
          <td>Framework 不關心 — parent 不在 component tree 內</td>
      </tr>
      <tr>
          <td>節點內的 children 不變</td>
          <td>Framework 不需要重建</td>
      </tr>
  </tbody>
</table>
<p>reconciliation 不會因為「位置變了」而重建節點 — 重建只發生在「節點消失了 + 新節點出現」的情境。</p>
<h3 id="安全-reparent-的-do--dont">安全 reparent 的 do / don&rsquo;t</h3>
<table>
  <thead>
      <tr>
          <th>Do</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>parent.appendChild(node)</code> 整節點搬</td>
          <td>identity 保留</td>
      </tr>
      <tr>
          <td><code>parent.insertBefore(node, ref)</code> 整節點搬到特定位置</td>
          <td>identity 保留</td>
      </tr>
      <tr>
          <td>搬之前 <code>node.cloneNode(true)</code> 為複本（如果要保留原位）</td>
          <td>複本是新 identity、原節點仍由 framework 管</td>
      </tr>
      <tr>
          <td>搬完後不動 node 內部</td>
          <td>framework 繼續正常更新</td>
      </tr>
  </tbody>
</table>
<table>
  <thead>
      <tr>
          <th>Don&rsquo;t</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>parent.appendChild(node.firstChild)</code> 搬 framework 子節點</td>
          <td>把節點抽出原 parent、framework 認為消失了</td>
      </tr>
      <tr>
          <td><code>node.innerHTML = node.innerHTML</code> 重設內部</td>
          <td>創造一堆新 identity、framework 認不得</td>
      </tr>
      <tr>
          <td>搬完後在 node 內 appendChild 加東西</td>
          <td>加的東西不在 framework 認知中、被清</td>
      </tr>
      <tr>
          <td>搬完後改 node 內子節點的 text / attribute</td>
          <td>framework 在下次 patch 時 revert</td>
      </tr>
  </tbody>
</table>
<p><strong>核心規則</strong>：搬節點 = 操作 node 本身；不要操作 node 的 children。</p>
<h3 id="跟-framework-reactivity-的對齊">跟 framework reactivity 的對齊</h3>
<p>某些 framework 對節點的「內部值」是 reactive 的（例如 <code>&lt;input&gt;</code> 的 <code>value</code>），改了會被 reconcile 回來。對這類屬性：</p>
<table>
  <thead>
      <tr>
          <th>屬性類型</th>
          <th>操作策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reactive value（input.value、textContent）</td>
          <td>透過 framework API 改、不要直接改 DOM</td>
      </tr>
      <tr>
          <td>純展示 attribute（class、aria-* 多數情境）</td>
          <td>直接改 DOM 通常 OK、但仍是灰區（見下節）</td>
      </tr>
      <tr>
          <td>Layout-relevant style（display、position）</td>
          <td>直接改 DOM 通常 OK、可能需要 fail-safe</td>
      </tr>
  </tbody>
</table>
<p>不確定某屬性是否 reactive：讀框架 source / 文件確認、或加 fail-safe 防意外。</p>
<hr>
<h2 id="改節點內部為什麼不安全有什麼例外">改節點內部：為什麼不安全、有什麼例外</h2>
<h3 id="為什麼不安全">為什麼不安全</h3>
<p>在 framework 管的節點內 appendChild / removeChild — framework 不認得這些操作的結果、下次 patch 時：</p>
<ol>
<li>Framework 比對 component tree 的目標狀態</li>
<li>看到 DOM 多了不該有的節點 → 移除</li>
<li>或看到 DOM 少了該有的節點 → 重建</li>
</ol>
<p>我們手動加的節點屬於前者、被移除。我們手動移除的子節點屬於後者、被重建（且重建的 identity 不同）。</p>
<h3 id="唯一的例外靜態元件--確認-patch-不重設">唯一的例外：靜態元件 + 確認 patch 不重設</h3>
<p>如果該 framework 子樹「初次 mount 後不再 patch」、改內部可能安全。但這是<strong>框架實作細節、隨版本可能變動</strong>。</p>
<p>例：當前 pagefind 的 filter 順序在初次 mount 時生成、後續 patch 不重排 — 所以 reorder filter 子節點實際安全。但這是「當前版本碰巧」、不是「框架保證」。</p>
<p><strong>操作規則</strong>：不要依賴「碰巧安全」。如果必須改內部、加 MutationObserver 監聽 framework 是否 revert、必要時補打。</p>
<hr>
<h2 id="改節點-attribute--inline-style灰區的-fail-safe-設計">改節點 attribute / inline style：灰區的 fail-safe 設計</h2>
<h3 id="為什麼是灰區">為什麼是灰區</h3>
<p>Framework 通常<strong>不主動管理節點的 inline style 與非 reactive attribute</strong> — 但「通常」不是「永遠」。某些 framework 會在 patch 時把 inline style 重設、或把 attribute 跟 component state 強制同步。</p>
<h3 id="fail-safe-工具-1important-提升優先級">Fail-safe 工具 1：<code>!important</code> 提升優先級</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s1">&#39;display&#39;</span><span class="p">,</span> <span class="s1">&#39;none&#39;</span><span class="p">,</span> <span class="s1">&#39;important&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>important</code> 把 inline style 的優先級提升 — 即使 framework 套了同屬性的低優先 style、也蓋不過。</p>
<h3 id="fail-safe-工具-2mutationobserver-補打">Fail-safe 工具 2：MutationObserver 補打</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">reapply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">setProperty</span><span class="p">(</span><span class="s1">&#39;display&#39;</span><span class="p">,</span> <span class="s1">&#39;none&#39;</span><span class="p">,</span> <span class="s1">&#39;important&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">reapply</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">reapply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">parent</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>Framework 在重繪後可能把 element 替換成新的 — observer 監聽到變動、立刻補套 style。</p>
<p>詳細設計（observer 範圍 / 觸發頻率 / self-mutation 處理）由 <a href="../mutation-observer-scope/">#29 MutationObserver 範圍與觸發頻率</a> 處理。</p>
<h3 id="fail-safe-工具-3css-class-toggle-取代-inline-style">Fail-safe 工具 3：CSS class toggle 取代 inline style</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 不用 inline style
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">toggle</span><span class="p">(</span><span class="s1">&#39;is-hidden&#39;</span><span class="p">);</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">/* CSS 內定義行為、layered CSS 不需要 important */</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">@</span><span class="k">layer</span> <span class="nt">base</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">.</span><span class="nc">is-hidden</span> <span class="p">{</span> <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>詳細展開由 <a href="../class-toggle-over-important/">#28 class toggle 取代 important</a> 處理。</p>
<p><strong>選擇順序</strong>：能用 class toggle 就用（最乾淨）；framework 會清 class 才用 inline + important + observer。</p>
<hr>
<h2 id="這次任務的邊界辨識實例">這次任務的邊界辨識實例</h2>
<p>四個 JS 操作場景、各有不同邊界：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>動的對象</th>
          <th>操作類別</th>
          <th>安全度</th>
          <th>處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>把 filter-panel 從 drawer 搬到 sidebar</td>
          <td>整節點 reparent</td>
          <td>1（安全）</td>
          <td>高</td>
          <td>直接搬、不動內部</td>
      </tr>
      <tr>
          <td>Reorder type / tag filter</td>
          <td>filter 子節點順序</td>
          <td>2（不安全）</td>
          <td>中 — 視 framework 而定</td>
          <td>確認框架不 reset 順序、加 observer 防護</td>
      </tr>
      <tr>
          <td>注入 scope UI</td>
          <td>自家新元件</td>
          <td>N/A（自家領域）</td>
          <td>高</td>
          <td>放 framework 邊界外（<a href="../coexisting-with-framework-managed-dom/">#5</a>）</td>
      </tr>
      <tr>
          <td>Filter 結果 hide / show</td>
          <td>pagefind 結果元素的 display</td>
          <td>3（灰區）</td>
          <td>中</td>
          <td>inline + important + observer 補打</td>
      </tr>
  </tbody>
</table>
<p>每個場景操作前的 mental check：「這是哪一類？該用什麼安全規則？」</p>
<hr>
<h2 id="設計取捨操作-framework-元件的策略">設計取捨：操作 framework 元件的策略</h2>
<p>四種策略、各自機會成本不同。預設追求「最高安全度的方式達成需求」、成本太高再降級。</p>
<h3 id="a完全不動-framework客製留邊界外這個專案的預設">A：完全不動 framework、客製留邊界外（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：把客製 UI 放在 framework sibling 位置、用 CSS 達成視覺效果</li>
<li><strong>選 A 的理由</strong>：跟 framework 完全解耦、命運自主</li>
<li><strong>適合</strong>：需求是「在 framework 旁加東西」（多數情境）</li>
<li><strong>代價</strong>：CSS 定位可能複雜</li>
<li><strong>詳細</strong>：<a href="../coexisting-with-framework-managed-dom/">#5 客製 UI 留 framework 邊界外</a></li>
</ul>
<h3 id="b整節點-reparent">B：整節點 reparent</h3>
<ul>
<li><strong>機制</strong>：把 framework 管的節點搬位置、不動內部</li>
<li><strong>跟 A 的取捨</strong>：A 不動 framework、B 搬 framework 元件本身；B 換到的是「能改變 framework 元件位置」、付出的是「節點內部仍由 framework 管、外部行為仍可能變」</li>
<li><strong>B 比 A 好的情境</strong>：framework 元件位置決定權需要奪回（例如 sidebar 切換）</li>
</ul>
<h3 id="c改節點-attribute--fail-safe">C：改節點 attribute + fail-safe</h3>
<ul>
<li><strong>機制</strong>：改 inline style / class、加 important + observer 補打</li>
<li><strong>跟 A/B 的取捨</strong>：A 不碰 framework、C 介入 framework 元件本身的視覺行為；C 比 A 侵入性高、但比直接改內部安全</li>
<li><strong>C 比 B 好的情境</strong>：需要的不是搬位置、是改顯隱 / 顏色 / state</li>
</ul>
<h3 id="d改節點內部最後手段">D：改節點內部（最後手段）</h3>
<ul>
<li><strong>機制</strong>：在 framework 子樹內 appendChild、改子節點屬性</li>
<li><strong>成本特別高的原因</strong>：跟 framework reconciliation 直接競爭、bug 不可預測、升級可能徹底打破</li>
<li><strong>D 才合理的情境</strong>：當前 framework 確認「該子樹不 reconcile」+ 升級時會重新驗證 — 通常不值得</li>
</ul>
<hr>
<h2 id="邊界宣告的實踐">邊界宣告的實踐</h2>
<h3 id="寫成-jsdoc-或-inline-註解">寫成 JSDoc 或 inline 註解</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cm"> * 把 .pagefind-ui__filter-panel 從 drawer 搬到外部 sidebar。
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="cm"> *
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="cm"> * 邊界：
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="cm"> *   - 動：filter-panel 整節點的 parent
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="cm"> *   - 不動：filter-panel 內部子節點（由 pagefind 管）
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="cm"> *   - State：checkbox 勾選由 pagefind 維護、跟著節點走
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="cm"> *
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="cm"> * 為什麼安全：節點 identity 不變、pagefind 在下次 patch
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="cm"> * 時看到節點還在、繼續更新內部。
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">sidebar</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>註解是給未來的自己 / 同事看的「契約備忘」 — 看到操作時知道為什麼安全。</p>
<hr>
<h2 id="跟其他原則的關係">跟其他原則的關係</h2>
<table>
  <thead>
      <tr>
          <th>抽象層原則</th>
          <th>關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../external-component-collaboration-layers/">#45 跟外部組件合作的層次</a></td>
          <td>本篇是「邊界內 DOM 層」操作的具體規則 — 接受要進入這層、用本篇規則限制傷害</td>
      </tr>
      <tr>
          <td><a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a></td>
          <td>操作範圍越小越安全 — 整節點 reparent 比改內部範圍小、改 attribute 比改子樹範圍小</td>
      </tr>
      <tr>
          <td><a href="../coexisting-with-framework-managed-dom/">#5 客製 UI 留邊界外</a></td>
          <td>互補關係 — #5 處理「不動 framework 的策略」、本篇處理「必須動 framework 時的安全規則」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>邊界問題</th>
          <th>第一個該檢查的事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>JS 操作後 framework 行為異常</td>
          <td>動到內部子節點</td>
          <td>確認操作只動「整節點 identity」、不動內部</td>
      </tr>
      <tr>
          <td>Inline style 在某些互動後消失</td>
          <td>動到 framework 管的 attribute</td>
          <td>加 observer 補打、或改用 CSS class toggle</td>
      </tr>
      <tr>
          <td>reparent 後 framework state 重置</td>
          <td>整節點移動但 framework 看作刪除</td>
          <td>確認框架對節點 identity 的追蹤機制（少數框架不靠 identity）</td>
      </tr>
      <tr>
          <td>某些 querySelector 命中不該命中的元素</td>
          <td>Selector 範圍超過自家元件</td>
          <td>把 query 限縮到 self 元件根節點下（<a href="../dom-selector-precision/">#14 selector 精準度</a>）</td>
      </tr>
      <tr>
          <td>「再加一段防禦邏輯應該就好了」第 2 次</td>
          <td>整體策略可能該換層級（從 D 升到 C 或 B）</td>
          <td><a href="../two-occurrence-threshold/">#42 2 次門檻</a>、考慮換策略</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：JS 動 framework 元件前、邊界先界定、選對應的安全規則。預設追求「完全不動 framework」(A)、必須動時用層級遞減的策略（B / C / D）— 每往下一層付的是「跟 framework 競爭」的成本。</p>
]]></content:encoded></item><item><title>Selector 精準度：讓 query 只命中你想要的元素</title><link>https://tarrragon.github.io/blog/report/dom-selector-precision/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/dom-selector-precision/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>JS 的 DOM query 從具體開始、發現不夠用再放寬。&lt;/strong> Selector 涵蓋「最少必要範圍」、避免誤命中其他元素、避免未來頁面結構變動讓 query 撈到不該撈的東西。精準度有三個收斂維度：起點（從哪開始找）、範圍（找多深）、過濾（哪些不要）— 三者一起設計才完整。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼精準度是-default">為什麼精準度是 default&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>DOM selector 的範圍越寬、被誤命中的可能性越高。寬泛 selector 像「網撈」 — 當下頁面只有一個目標元素時看不出問題、未來頁面結構變動（加第二個同類元件、加 demo 區塊、加 widget）就壞。&lt;/p>
&lt;p>精準度的成本是「寫 selector 時多想一點」、收益是「行為可預測、不會被未來變動打破」。&lt;strong>這不是優化、是 sanity 防線&lt;/strong>。&lt;/p>
&lt;h3 id="寬泛-selector-的失敗模式">寬泛 selector 的失敗模式&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗模式&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>根因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>跨元件誤命中&lt;/td>
 &lt;td>該動的動了、不該動的也動了&lt;/td>
 &lt;td>沒指定 ancestor scope&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同名 class 誤命中&lt;/td>
 &lt;td>demo 區塊 / 文檔截圖也被處理&lt;/td>
 &lt;td>沒過濾「處於展示用途」的元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>未初始化元素被處理&lt;/td>
 &lt;td>元件還沒 mount 完就被操作&lt;/td>
 &lt;td>沒過濾「狀態未就緒」的元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已處理元素重複處理&lt;/td>
 &lt;td>apply 被 observer 觸發又處理一次&lt;/td>
 &lt;td>沒標記「已處理」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四種失敗都來自「query 範圍 &amp;gt; 真實需要的範圍」。從具體開始就避免。&lt;/p>
&lt;hr>
&lt;h2 id="三層收斂維度">三層收斂維度&lt;/h2>
&lt;p>Selector 精準度不是單一參數、是三個維度的組合。每個維度都該設計、不能只想其中一個。&lt;/p>
&lt;h3 id="維度-1起點從哪個-root-開始找">維度 1：起點（從哪個 root 開始找）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：query 的起點決定「最大可能範圍」。從 &lt;code>document&lt;/code> 起 = 全頁面；從元件根起 = 子樹內。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 寬：全頁面搜尋
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 收斂：從元件根開始
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>從元件根開始等於把 selector 的作用範圍收斂到「我管的子樹」 — 即使未來頁面其他地方出現同名元素、跟我無關。&lt;/p>
&lt;p>&lt;strong>起點選擇的決策&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>起點&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>document&lt;/code>&lt;/td>
 &lt;td>確定全頁只有一個目標、且未來不會增加同類&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件根（變數存好）&lt;/td>
 &lt;td>一般情境（推薦預設）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>函式參數傳入根&lt;/td>
 &lt;td>同頁面有多個元件實例、各自獨立 setup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事件 &lt;code>closest&lt;/code> 反向找根&lt;/td>
 &lt;td>動態多實例、用事件驅動&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>多元件 setup pattern&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 其他 setup
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>頁面有 N 個 shell、自動 setup N 次、各自獨立。當前只一個也適用、未來加更多無痛 — 這是「起點當參數」帶來的擴展性。&lt;/p>
&lt;p>&lt;strong>例外處理&lt;/strong>：當目標元素不在元件子樹內（例如同層的 sibling），保留 &lt;code>document.querySelector&lt;/code> 但加註解說明：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// slot 是 main 的子節點、跟 shell 同層、不能從 shell 找
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">slot&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-filter-slot&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>註解讓未來維護者知道這是「明知故為」的例外、不是疏忽。&lt;/p>
&lt;h3 id="維度-2範圍找多深">維度 2：範圍（找多深）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：起點確定後、要找直接子、特定層、還是任意深度。&lt;/p>
&lt;p>&lt;code>querySelector&lt;/code> 預設找任意深度 — 大部分情況沒問題、但結構穩定時可以更精準：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 預設：任意深度
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 限縮：只找直接子
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;:scope &amp;gt; .pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">// 限縮：只找特定層
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;:scope &amp;gt; div &amp;gt; .pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>:scope&lt;/code> 在 querySelector 內表示 query 的起始元素 — 配合 &lt;code>&amp;gt;&lt;/code> 就能精準匹配「直接子」。&lt;/p>
&lt;p>&lt;strong>範圍選擇的決策&lt;/strong>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>範圍&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>任意深度（預設）&lt;/td>
 &lt;td>結構可能變動、目標可能搬位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>直接子 &lt;code>:scope &amp;gt; X&lt;/code>&lt;/td>
 &lt;td>結構穩定、避免深層誤命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>特定路徑 &lt;code>:scope &amp;gt; A &amp;gt; B&lt;/code>&lt;/td>
 &lt;td>結構非常穩定、想要結構變動立即察覺&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選太寬未來誤命中、選太窄未來結構微調就壞 — 預設選任意深度、結構穩定的關鍵 query 才用 &lt;code>:scope &amp;gt;&lt;/code>。&lt;/p>
&lt;h3 id="維度-3過濾哪些元素不要">維度 3：過濾（哪些元素不要）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：起點 + 範圍確定後、可能還是命中過多 — 用 attribute filter 與否定 selector 排除不要的。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>JS 的 DOM query 從具體開始、發現不夠用再放寬。</strong> Selector 涵蓋「最少必要範圍」、避免誤命中其他元素、避免未來頁面結構變動讓 query 撈到不該撈的東西。精準度有三個收斂維度：起點（從哪開始找）、範圍（找多深）、過濾（哪些不要）— 三者一起設計才完整。</p>
<hr>
<h2 id="為什麼精準度是-default">為什麼精準度是 default</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>DOM selector 的範圍越寬、被誤命中的可能性越高。寬泛 selector 像「網撈」 — 當下頁面只有一個目標元素時看不出問題、未來頁面結構變動（加第二個同類元件、加 demo 區塊、加 widget）就壞。</p>
<p>精準度的成本是「寫 selector 時多想一點」、收益是「行為可預測、不會被未來變動打破」。<strong>這不是優化、是 sanity 防線</strong>。</p>
<h3 id="寬泛-selector-的失敗模式">寬泛 selector 的失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>失敗模式</th>
          <th>表現</th>
          <th>根因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>跨元件誤命中</td>
          <td>該動的動了、不該動的也動了</td>
          <td>沒指定 ancestor scope</td>
      </tr>
      <tr>
          <td>同名 class 誤命中</td>
          <td>demo 區塊 / 文檔截圖也被處理</td>
          <td>沒過濾「處於展示用途」的元素</td>
      </tr>
      <tr>
          <td>未初始化元素被處理</td>
          <td>元件還沒 mount 完就被操作</td>
          <td>沒過濾「狀態未就緒」的元素</td>
      </tr>
      <tr>
          <td>已處理元素重複處理</td>
          <td>apply 被 observer 觸發又處理一次</td>
          <td>沒標記「已處理」</td>
      </tr>
  </tbody>
</table>
<p>四種失敗都來自「query 範圍 &gt; 真實需要的範圍」。從具體開始就避免。</p>
<hr>
<h2 id="三層收斂維度">三層收斂維度</h2>
<p>Selector 精準度不是單一參數、是三個維度的組合。每個維度都該設計、不能只想其中一個。</p>
<h3 id="維度-1起點從哪個-root-開始找">維度 1：起點（從哪個 root 開始找）</h3>
<p><strong>核心定義</strong>：query 的起點決定「最大可能範圍」。從 <code>document</code> 起 = 全頁面；從元件根起 = 子樹內。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 寬：全頁面搜尋
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 收斂：從元件根開始
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">);</span></span></span></code></pre></div><p>從元件根開始等於把 selector 的作用範圍收斂到「我管的子樹」 — 即使未來頁面其他地方出現同名元素、跟我無關。</p>
<p><strong>起點選擇的決策</strong>：</p>
<table>
  <thead>
      <tr>
          <th>起點</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>document</code></td>
          <td>確定全頁只有一個目標、且未來不會增加同類</td>
      </tr>
      <tr>
          <td>元件根（變數存好）</td>
          <td>一般情境（推薦預設）</td>
      </tr>
      <tr>
          <td>函式參數傳入根</td>
          <td>同頁面有多個元件實例、各自獨立 setup</td>
      </tr>
      <tr>
          <td>事件 <code>closest</code> 反向找根</td>
          <td>動態多實例、用事件驅動</td>
      </tr>
  </tbody>
</table>
<p><strong>多元件 setup pattern</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">ui</span>     <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 其他 setup
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span></span></span></code></pre></div><p>頁面有 N 個 shell、自動 setup N 次、各自獨立。當前只一個也適用、未來加更多無痛 — 這是「起點當參數」帶來的擴展性。</p>
<p><strong>例外處理</strong>：當目標元素不在元件子樹內（例如同層的 sibling），保留 <code>document.querySelector</code> 但加註解說明：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// slot 是 main 的子節點、跟 shell 同層、不能從 shell 找
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">slot</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">);</span></span></span></code></pre></div><p>註解讓未來維護者知道這是「明知故為」的例外、不是疏忽。</p>
<h3 id="維度-2範圍找多深">維度 2：範圍（找多深）</h3>
<p><strong>核心定義</strong>：起點確定後、要找直接子、特定層、還是任意深度。</p>
<p><code>querySelector</code> 預設找任意深度 — 大部分情況沒問題、但結構穩定時可以更精準：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 預設：任意深度
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 限縮：只找直接子
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;:scope &gt; .pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">// 限縮：只找特定層
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;:scope &gt; div &gt; .pagefind-ui&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>:scope</code> 在 querySelector 內表示 query 的起始元素 — 配合 <code>&gt;</code> 就能精準匹配「直接子」。</p>
<p><strong>範圍選擇的決策</strong>：</p>
<table>
  <thead>
      <tr>
          <th>範圍</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>任意深度（預設）</td>
          <td>結構可能變動、目標可能搬位置</td>
      </tr>
      <tr>
          <td>直接子 <code>:scope &gt; X</code></td>
          <td>結構穩定、避免深層誤命中</td>
      </tr>
      <tr>
          <td>特定路徑 <code>:scope &gt; A &gt; B</code></td>
          <td>結構非常穩定、想要結構變動立即察覺</td>
      </tr>
  </tbody>
</table>
<p>選太寬未來誤命中、選太窄未來結構微調就壞 — 預設選任意深度、結構穩定的關鍵 query 才用 <code>:scope &gt;</code>。</p>
<h3 id="維度-3過濾哪些元素不要">維度 3：過濾（哪些元素不要）</h3>
<p><strong>核心定義</strong>：起點 + 範圍確定後、可能還是命中過多 — 用 attribute filter 與否定 selector 排除不要的。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 寬：所有 result
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 過濾：只取已 rank 過的（排除初始化中的）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result[data-pagefind-rank]&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">// 過濾：排除已處理過的
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result:not([data-scoped])&#39;</span><span class="p">);</span></span></span></code></pre></div><p><strong>過濾技巧</strong>：</p>
<table>
  <thead>
      <tr>
          <th>技巧</th>
          <th>用法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Attribute filter</td>
          <td><code>[data-state=&quot;ready&quot;]</code> 只取狀態就緒的</td>
      </tr>
      <tr>
          <td><code>:not()</code> 排除</td>
          <td><code>:not([data-scoped])</code> 排除已處理</td>
      </tr>
      <tr>
          <td>Attribute exists</td>
          <td><code>[data-pagefind-rank]</code> 只取有特定屬性的</td>
      </tr>
      <tr>
          <td>處理後標記</td>
          <td>處理完 <code>el.setAttribute('data-scoped', 'true')</code> 避免重複處理</td>
      </tr>
  </tbody>
</table>
<p><strong>「處理後標記」是 idempotency 工具</strong>：apply 函式可能被多次呼叫（observer 觸發、event 觸發），標記 + <code>:not()</code> 過濾確保每個元素只處理一次。</p>
<hr>
<h2 id="三維度的組合範例">三維度的組合範例</h2>
<p>完整的精準 selector 設計：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>           <span class="c1">// 維度 1：起點
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">var</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span>                          <span class="c1">// 維度 2：任意深度
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>  <span class="s1">&#39;.pagefind-ui__result[data-pagefind-rank]:not([data-scoped])&#39;</span>  <span class="c1">// 維度 3：過濾
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">results</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>                      <span class="c1">// 處理後標記
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>每個維度都有意識地選擇 — 不是把所有預設值疊一起。</p>
<hr>
<h2 id="內在屬性比較四種-selector-設計">內在屬性比較：四種 selector 設計</h2>
<table>
  <thead>
      <tr>
          <th>設計</th>
          <th>誤命中風險</th>
          <th>未來結構變動的容忍度</th>
          <th>多元件支援</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>document.querySelector('.x')</code></td>
          <td>高</td>
          <td>低 — 任何同名出現就壞</td>
          <td>否（只取第一個）</td>
      </tr>
      <tr>
          <td><code>shell.querySelector('.x')</code></td>
          <td>低</td>
          <td>中 — shell 內變動才影響</td>
          <td>部分</td>
      </tr>
      <tr>
          <td><code>shell.querySelector(':scope &gt; .x')</code></td>
          <td>最低</td>
          <td>低 — 結構微調就壞</td>
          <td>部分</td>
      </tr>
      <tr>
          <td>起點當參數 + 過濾 + 標記</td>
          <td>最低</td>
          <td>高 — 顯式聲明所有假設</td>
          <td>完整</td>
      </tr>
  </tbody>
</table>
<p><strong>推薦</strong>：起點當參數 + 過濾。<code>:scope &gt;</code> 只在「結構保證穩定」的關鍵 query 用。</p>
<hr>
<h2 id="進階技巧">進階技巧</h2>
<h3 id="1-把元件根存成變數一次">1. 把元件根存成變數一次</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// 之後所有 query 都從 shell 開始
</span></span></span></code></pre></div><p>避免每次 query 都重新從 document 找元件根 — 一是效能（小）、二是 query 範圍仍維持在 shell 內。</p>
<h3 id="2-用-closest-反向找根">2. 用 closest 反向找根</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">getShell</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">return</span> <span class="nx">el</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">getShell</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="c1">// 在這個 shell 內處理
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>事件委派 + closest 適合「多元件實例 + 動態事件處理」 — 各 shell 不需要各自綁 listener、共用一個 listener 用 closest 區分。</p>
<h3 id="3-起點不存在時提早-return">3. 起點不存在時提早 return</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span></span></span></code></pre></div><p>頁面可能沒有 shell（不是搜尋頁），所有後續 query 都會失敗。提早 return 比後續一連串 null check 乾淨。</p>
<h3 id="4-weakmap-替代-attribute-標記">4. WeakMap 替代 attribute 標記</h3>
<p>當不想污染 DOM attribute 時、用 WeakMap 紀錄已處理的元素：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>WeakMap 在元素 GC 時自動清理、不留下 DOM 痕跡。適合短生命週期的 idempotency。</p>
<hr>
<h2 id="設計取捨起點選擇">設計取捨：起點選擇</h2>
<p>Selector 的「起點」有四種做法、各自機會成本不同。這個專案選 B（元件根存變數）當預設、其他做法在特定情境也合理。每張卡片獨立展開該做法的設計細節。</p>
<h3 id="adocumentqueryselector">A：<a href="../pattern-document-query/"><code>document.querySelector</code> 全文件搜</a></h3>
<ul>
<li><strong>機制</strong>：每處 query 都從 document 開始、靠 class name 唯一性命中目標</li>
<li><strong>適合</strong>：原型階段、demo 程式碼、確定全頁只有一個目標且未來不會變</li>
<li><strong>代價</strong>：未來頁面結構變動（加同類 widget、加 demo 區塊）就壞、且失敗模式是安靜地操作錯元素、不報錯</li>
<li><strong>選 A 的時機</strong>：「快速看會不會動」的探索期</li>
</ul>
<h3 id="b元件根存變數之後從變數-query這個專案的預設">B：<a href="../pattern-component-root/">元件根存變數、之後從變數 query</a>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>var shell = document.querySelector('.search-shell')</code> 一次、之後所有 query 用 <code>shell.querySelector(...)</code></li>
<li><strong>選 B 的理由</strong>：當前頁面只有一個 shell、未來可能加（站內搜尋 widget、相關搜尋）— 用變數隔離成本低、提早預防</li>
<li><strong>適合</strong>：一般客製情境、預期未來結構可能擴展</li>
<li><strong>代價</strong>：多一個變數、多一次 query;函式內邏輯變得依賴外部變數</li>
</ul>
<h3 id="c函式接受元件根當參數">C：<a href="../pattern-root-as-parameter/">函式接受元件根當參數</a></h3>
<ul>
<li><strong>機制</strong>：<code>function setup(shell) { shell.querySelector(...) }</code>、外部呼叫 <code>document.querySelectorAll('.shell').forEach(setup)</code></li>
<li><strong>跟 B 的取捨</strong>：B 假設只有一個 shell、C 直接支援多 shell；C 的設計成本前期較高（每函式多一個參數）、但多實例支援是免費的</li>
<li><strong>C 比 B 好的情境</strong>：頁面同時有多個 shell（例如多語切換頁面）、或計劃中要重用組件到不同頁面</li>
</ul>
<h3 id="d事件-">D：<a href="../pattern-closest-lookup/">事件 + <code>closest</code> 反向找根</a></h3>
<ul>
<li><strong>機制</strong>：監聽全域事件、事件處理時 <code>e.target.closest('.shell')</code> 反向找元件根</li>
<li><strong>跟 B/C 的取捨</strong>：B/C 是「初始化時綁定」、D 是「事件發生時動態判斷」— D 適合元件動態出現 / 消失（SPA 路由切換、AJAX 注入）</li>
<li><strong>D 比 C 好的情境</strong>：元件實例在 runtime 動態增減、用 mutation observer 補打成本反而更高</li>
<li><strong>代價</strong>：事件委派的調試比直接綁定難（不知道事件實際從哪傳上來）</li>
</ul>
<hr>
<h2 id="設計取捨範圍深度">設計取捨：範圍深度</h2>
<p><code>querySelector</code> 預設找任意深度、可以收緊到直接子。三種做法：</p>
<h3 id="a任意深度這個專案的預設">A：任意深度（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>shell.querySelector('.target')</code> — 子樹任何深度都接受</li>
<li><strong>選 A 的理由</strong>：結構可能因 framework 升級微調、容忍微調換取維護彈性</li>
<li><strong>代價</strong>：深層結構意外多出同名元素時可能誤命中</li>
</ul>
<h3 id="b直接子-scope--x">B：直接子 <code>:scope &gt; X</code></h3>
<ul>
<li><strong>機制</strong>：<code>shell.querySelector(':scope &gt; .target')</code> — 只找直接子</li>
<li><strong>跟 A 的取捨</strong>：A 容忍結構微調、B 強制結構穩定 — B 帶來「結構變動立即報錯」的早期偵測</li>
<li><strong>B 比 A 好的情境</strong>：自家完全控制的結構、想用 selector 失敗當回歸測試訊號</li>
</ul>
<h3 id="c特定路徑-scope--a--b">C：特定路徑 <code>:scope &gt; A &gt; B</code></h3>
<ul>
<li><strong>機制</strong>：強制一條精確路徑</li>
<li><strong>代價</strong>：結構任何微調都壞、維護成本高</li>
<li><strong>C 才合理的情境</strong>：寫整合測試的結構斷言、不是 production query</li>
</ul>
<hr>
<h2 id="設計取捨過濾與-idempotency">設計取捨：過濾與 idempotency</h2>
<p>apply 函式可能被多次觸發（observer / event / 初始化）、過濾保證每元素只處理一次。三種做法：</p>
<h3 id="adom-attribute-標記這個專案的預設">A：<a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>:not([data-scoped])</code> 過濾 + 處理後 <code>el.setAttribute('data-scoped', 'true')</code></li>
<li><strong>選 A 的理由</strong>：標記跟著 DOM 元素走、元素被移除時自動清理；標記在 devtools 可見、debug 直接</li>
<li><strong>代價</strong>：DOM 上多了一個自家用的 attribute（命名衝突風險小）</li>
</ul>
<h3 id="bweakmap-紀錄">B：<a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a></h3>
<ul>
<li><strong>機制</strong>：<code>var processed = new WeakMap(); processed.set(el, true)</code></li>
<li><strong>跟 A 的取捨</strong>：B 不污染 DOM、適合「不想留 attribute 痕跡」的場景；A 在 devtools 可見、debug 較直接</li>
<li><strong>B 比 A 好的情境</strong>：寫成第三方函式庫、不想對使用者 DOM 加屬性</li>
</ul>
<h3 id="c依賴外部呼叫者保證只呼叫一次">C：依賴外部呼叫者保證只呼叫一次</h3>
<ul>
<li><strong>機制</strong>：apply 內不防護、依賴 init 時只綁一次 listener</li>
<li><strong>成本特別高的原因</strong>：observer 觸發 / 事件觸發 / 初始化任一處多呼叫、就產生重複處理 bug；錯誤難以追蹤</li>
<li><strong>C 才合理的情境</strong>：apply 本身是 idempotent 的（例如 set class 設成已是的值、無副作用）— 此時不需過濾</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Selector 精準度問題</th>
          <th>修正動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多處 <code>document.querySelector</code> 同類元素</td>
          <td>起點太寬</td>
          <td>把元件根存變數、之後 query 從變數開始</td>
      </tr>
      <tr>
          <td>同頁加第二個元件實例後行為錯亂</td>
          <td>起點 hardcode</td>
          <td>改「起點當參數」pattern</td>
      </tr>
      <tr>
          <td>Selector 命中了不該命中的元素</td>
          <td>範圍 / 過濾不足</td>
          <td>加 ancestor scope、或加 attribute filter</td>
      </tr>
      <tr>
          <td>Apply 被多次呼叫產生重複處理</td>
          <td>沒 idempotency 防線</td>
          <td>加 <code>:not([data-flag])</code> + 處理後標記</td>
      </tr>
      <tr>
          <td>結構微調後 selector 失效</td>
          <td><code>:scope &gt;</code> 用得太死</td>
          <td>換成任意深度（預設）</td>
      </tr>
      <tr>
          <td>事件處理時不知是哪個元件實例</td>
          <td>沒反向找根機制</td>
          <td>用 <code>closest</code></td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Selector 精準度不是極致最佳化、是 sanity 防線。三維度（起點 / 範圍 / 過濾）一起設計、每個維度都顯式選擇 — 比從寬泛開始一路追 bug 容易得多。</p>
<p>寬 selector（<code>querySelectorAll('.title')</code>）是「便利位置」、窄 selector 是「對齊位置」 — 這個反相關的更高層原則見 <a href="../ease-of-writing-vs-intent-alignment/">#67 寫作便利度跟意圖對齊反相關</a>。</p>
]]></content:encoded></item><item><title>MutationObserver 範圍與觸發頻率：監聽最少必要的變動</title><link>https://tarrragon.github.io/blog/report/mutation-observer-scope/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/mutation-observer-scope/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>MutationObserver 監聽最少必要的變動 — 從「監聽哪個 root」「觀察什麼類型」「多久觸發一次」三維度收斂。&lt;/strong> 範圍寬會頻繁觸發、option 勾多會在不關心的變動上跑邏輯、apply 自己改 DOM 會引發無限循環。三維度都該顯式設計、不能只丟預設。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-observer-需要獨立議題">為什麼 observer 需要獨立議題&lt;/h2>
&lt;h3 id="跟-selector-的差異">跟 selector 的差異&lt;/h3>
&lt;p>Observer 與 selector 都涉及「DOM 範圍」、機制完全不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Selector&lt;/th>
 &lt;th>Observer&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>時機&lt;/td>
 &lt;td>同步、當下查詢&lt;/td>
 &lt;td>非同步、回應未來變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>執行頻率&lt;/td>
 &lt;td>一次或顯式重呼叫&lt;/td>
 &lt;td>隨 DOM 變動自動觸發&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗模式&lt;/td>
 &lt;td>撈太多 / 撈太少&lt;/td>
 &lt;td>觸發太頻繁 / 漏觸發 / 無限循環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設計重點&lt;/td>
 &lt;td>起點 + 範圍 + 過濾&lt;/td>
 &lt;td>監聽範圍 + option + 頻率&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>把 selector 與 observer 綁同一篇討論會混淆 — 兩者解決的是不同問題、有不同失敗模式、需要不同的設計工具。&lt;/p>
&lt;h3 id="observer-寬範圍的失敗模式">Observer 寬範圍的失敗模式&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>失敗模式&lt;/th>
 &lt;th>表現&lt;/th>
 &lt;th>根因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>過度觸發&lt;/td>
 &lt;td>短時間觸發數十次&lt;/td>
 &lt;td>subtree 太深 + option 太多&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>在錯時機跑&lt;/td>
 &lt;td>layout 還沒穩就跑 apply&lt;/td>
 &lt;td>沒等 framework patch 結束&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>無限循環&lt;/td>
 &lt;td>apply 自己改 DOM 又觸發 observer&lt;/td>
 &lt;td>沒 disconnect/observe 保護&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>漏掉變動&lt;/td>
 &lt;td>預期會觸發但沒觸發&lt;/td>
 &lt;td>option 沒勾對、或 root 選錯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>四種都來自「沒精細設計 observer 的監聽形狀」。&lt;/p>
&lt;hr>
&lt;h2 id="三維度收斂">三維度收斂&lt;/h2>
&lt;h3 id="維度-1監聽哪個-root範圍">維度 1：監聽哪個 root（範圍）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：observer 的 root 元素決定「哪些範圍內的變動會被看到」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 寬：監聽整個 .pagefind-ui
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">apply&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ui&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 收斂：只監聽結果列表
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">results&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__results&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">apply&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">results&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>寬範圍把無關變動也帶進來 — pagefind 重繪 input、調整 filter、重排 chip 都會觸發 apply、但 apply 只關心結果變動。&lt;/p>
&lt;p>&lt;strong>Root 選擇的決策&lt;/strong>：找到「&lt;strong>包含所有目標變動、但不包含其他無關變動&lt;/strong>的最小元素」。&lt;/p>
&lt;ul>
&lt;li>太大 → 帶進無關變動、過度觸發&lt;/li>
&lt;li>太小 → 漏掉真正關心的變動&lt;/li>
&lt;li>剛好 → 只關心的變動觸發&lt;/li>
&lt;/ul>
&lt;p>問自己：「我關心的變動發生在哪些元素？這些元素的最小共同 ancestor 是誰？」答案就是 observer root。&lt;/p>
&lt;h3 id="維度-2觀察什麼類型option-flag">維度 2：觀察什麼類型（option flag）&lt;/h3>
&lt;p>&lt;strong>核心定義&lt;/strong>：MutationObserver 提供四種 option、每種對應不同類型變動：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 子節點增 / 減 / 重排
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">attributes&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 屬性變動
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">attributeFilter&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;data-state&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="c1">// 只看特定屬性
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">characterData&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 文字內容變動
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 上面三種往子樹深處看
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">attributeOldValue&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1">// 屬性變動時記錄舊值
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">characterDataOldValue&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設只勾需要的、不要全部 true：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Option&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>觸發頻率&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>childList: true&lt;/code>&lt;/td>
 &lt;td>子節點增減&lt;/td>
 &lt;td>中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>childList + subtree&lt;/code>&lt;/td>
 &lt;td>任何深度的子節點增減&lt;/td>
 &lt;td>高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>attributes&lt;/code> 全屬性&lt;/td>
 &lt;td>任何屬性變動&lt;/td>
 &lt;td>最高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>attributes + attributeFilter&lt;/code>&lt;/td>
 &lt;td>只特定屬性&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>characterData&lt;/code>&lt;/td>
 &lt;td>文字內容（少用）&lt;/td>
 &lt;td>低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>避免勾 subtree&lt;/strong>：subtree 把監聽從「直接子」擴展到「整個子樹」、觸發頻率可能爆炸。只在「真的需要看深層變動」時用。&lt;/p>
&lt;p>&lt;strong>避免無 filter 的 attributes&lt;/strong>：DOM 屬性變動很頻繁（class 改、style 改、aria-* 改），不過濾會被淹沒。用 &lt;code>attributeFilter: [...]&lt;/code> 縮到只看你關心的屬性。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>MutationObserver 監聽最少必要的變動 — 從「監聽哪個 root」「觀察什麼類型」「多久觸發一次」三維度收斂。</strong> 範圍寬會頻繁觸發、option 勾多會在不關心的變動上跑邏輯、apply 自己改 DOM 會引發無限循環。三維度都該顯式設計、不能只丟預設。</p>
<hr>
<h2 id="為什麼-observer-需要獨立議題">為什麼 observer 需要獨立議題</h2>
<h3 id="跟-selector-的差異">跟 selector 的差異</h3>
<p>Observer 與 selector 都涉及「DOM 範圍」、機制完全不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Selector</th>
          <th>Observer</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>時機</td>
          <td>同步、當下查詢</td>
          <td>非同步、回應未來變動</td>
      </tr>
      <tr>
          <td>執行頻率</td>
          <td>一次或顯式重呼叫</td>
          <td>隨 DOM 變動自動觸發</td>
      </tr>
      <tr>
          <td>失敗模式</td>
          <td>撈太多 / 撈太少</td>
          <td>觸發太頻繁 / 漏觸發 / 無限循環</td>
      </tr>
      <tr>
          <td>設計重點</td>
          <td>起點 + 範圍 + 過濾</td>
          <td>監聽範圍 + option + 頻率</td>
      </tr>
  </tbody>
</table>
<p>把 selector 與 observer 綁同一篇討論會混淆 — 兩者解決的是不同問題、有不同失敗模式、需要不同的設計工具。</p>
<h3 id="observer-寬範圍的失敗模式">Observer 寬範圍的失敗模式</h3>
<table>
  <thead>
      <tr>
          <th>失敗模式</th>
          <th>表現</th>
          <th>根因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>過度觸發</td>
          <td>短時間觸發數十次</td>
          <td>subtree 太深 + option 太多</td>
      </tr>
      <tr>
          <td>在錯時機跑</td>
          <td>layout 還沒穩就跑 apply</td>
          <td>沒等 framework patch 結束</td>
      </tr>
      <tr>
          <td>無限循環</td>
          <td>apply 自己改 DOM 又觸發 observer</td>
          <td>沒 disconnect/observe 保護</td>
      </tr>
      <tr>
          <td>漏掉變動</td>
          <td>預期會觸發但沒觸發</td>
          <td>option 沒勾對、或 root 選錯</td>
      </tr>
  </tbody>
</table>
<p>四種都來自「沒精細設計 observer 的監聽形狀」。</p>
<hr>
<h2 id="三維度收斂">三維度收斂</h2>
<h3 id="維度-1監聽哪個-root範圍">維度 1：監聽哪個 root（範圍）</h3>
<p><strong>核心定義</strong>：observer 的 root 元素決定「哪些範圍內的變動會被看到」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 寬：監聽整個 .pagefind-ui
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">ui</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 收斂：只監聽結果列表
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__results&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">results</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>寬範圍把無關變動也帶進來 — pagefind 重繪 input、調整 filter、重排 chip 都會觸發 apply、但 apply 只關心結果變動。</p>
<p><strong>Root 選擇的決策</strong>：找到「<strong>包含所有目標變動、但不包含其他無關變動</strong>的最小元素」。</p>
<ul>
<li>太大 → 帶進無關變動、過度觸發</li>
<li>太小 → 漏掉真正關心的變動</li>
<li>剛好 → 只關心的變動觸發</li>
</ul>
<p>問自己：「我關心的變動發生在哪些元素？這些元素的最小共同 ancestor 是誰？」答案就是 observer root。</p>
<h3 id="維度-2觀察什麼類型option-flag">維度 2：觀察什麼類型（option flag）</h3>
<p><strong>核心定義</strong>：MutationObserver 提供四種 option、每種對應不同類型變動：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>        <span class="c1">// 子節點增 / 減 / 重排
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">attributes</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>       <span class="c1">// 屬性變動
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">attributeFilter</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;data-state&#39;</span><span class="p">],</span>  <span class="c1">// 只看特定屬性
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nx">characterData</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>    <span class="c1">// 文字內容變動
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>          <span class="c1">// 上面三種往子樹深處看
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="nx">attributeOldValue</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>  <span class="c1">// 屬性變動時記錄舊值
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"></span>  <span class="nx">characterDataOldValue</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>預設只勾需要的、不要全部 true：</p>
<table>
  <thead>
      <tr>
          <th>Option</th>
          <th>用途</th>
          <th>觸發頻率</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>childList: true</code></td>
          <td>子節點增減</td>
          <td>中</td>
      </tr>
      <tr>
          <td><code>childList + subtree</code></td>
          <td>任何深度的子節點增減</td>
          <td>高</td>
      </tr>
      <tr>
          <td><code>attributes</code> 全屬性</td>
          <td>任何屬性變動</td>
          <td>最高</td>
      </tr>
      <tr>
          <td><code>attributes + attributeFilter</code></td>
          <td>只特定屬性</td>
          <td>低</td>
      </tr>
      <tr>
          <td><code>characterData</code></td>
          <td>文字內容（少用）</td>
          <td>低</td>
      </tr>
  </tbody>
</table>
<p><strong>避免勾 subtree</strong>：subtree 把監聽從「直接子」擴展到「整個子樹」、觸發頻率可能爆炸。只在「真的需要看深層變動」時用。</p>
<p><strong>避免無 filter 的 attributes</strong>：DOM 屬性變動很頻繁（class 改、style 改、aria-* 改），不過濾會被淹沒。用 <code>attributeFilter: [...]</code> 縮到只看你關心的屬性。</p>
<h3 id="維度-3多久觸發一次頻率">維度 3：多久觸發一次（頻率）</h3>
<p><strong>核心定義</strong>：observer 的回呼可能短時間內被連續呼叫、用 debounce 把多次合併成一次。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">timer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">function</span> <span class="nx">schedule</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">clearTimeout</span><span class="p">(</span><span class="nx">timer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">timer</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(</span><span class="nx">apply</span><span class="p">,</span> <span class="mi">80</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">schedule</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>Debounce 80ms 表示「最後一次變動後 80ms 沒再變、才跑 apply」 — 把連續變動合併。</p>
<p><strong>Debounce vs Throttle</strong>：</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>行為</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Debounce</td>
          <td>安靜後執行</td>
          <td>等 framework 連續 patch 結束</td>
      </tr>
      <tr>
          <td>Throttle</td>
          <td>固定頻率執行</td>
          <td>UI 同步要立即反應、但限速</td>
      </tr>
      <tr>
          <td>立即執行</td>
          <td>每次都跑</td>
          <td>變動頻率本來就低、且每次都要處理</td>
      </tr>
  </tbody>
</table>
<p>大部分 observer 場景適合 debounce — framework patch 是突發性、不是持續的。</p>
<p><strong>Debounce 時間選擇</strong>：</p>
<table>
  <thead>
      <tr>
          <th>時間</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>16ms（一個 frame）</td>
          <td>跟 paint 同步、最即時</td>
      </tr>
      <tr>
          <td>50-100ms</td>
          <td>一般 UI 反應、肉眼感受不到延遲</td>
      </tr>
      <tr>
          <td>200-300ms</td>
          <td>等使用者輸入結束</td>
      </tr>
      <tr>
          <td>1000ms+</td>
          <td>後台處理、不影響 UI</td>
      </tr>
  </tbody>
</table>
<p>預設 50-100ms — 比一個 frame 寬、又不會讓使用者感受延遲。</p>
<hr>
<h2 id="self-mutation-循環的處理">Self-mutation 循環的處理</h2>
<h3 id="問題場景">問題場景</h3>
<p>apply 函式自己也改 DOM 時、會再次觸發 observer：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 改了某個元素的 class（attribute 變動）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">someEl</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;processed&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">attributes</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">// → apply 改 class 觸發 observer → observer 又呼叫 apply → 無限循環
</span></span></span></code></pre></div><p><strong>這不是邏輯錯、是 observer 機制的特性</strong>：observer 不會區分「是不是 apply 自己改的」。</p>
<h3 id="解法disconnect--observe-配對">解法：disconnect / observe 配對</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">observer</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>      <span class="c1">// 暫停監聽
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">apply</span><span class="p">();</span>                    <span class="c1">// 自己改 DOM 不會觸發
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>  <span class="c1">// 恢復監聽
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span></span></span></code></pre></div><p>apply 期間 observer 暫停、apply 結束後恢復 — 自己的改動不會觸發自己。</p>
<h3 id="解法替代用-attribute-標記區分">解法替代：用 attribute 標記區分</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">isApplying</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">someEl</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;processed&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">isApplying</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isApplying</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="nx">apply</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span></span></span></code></pre></div><p>但這個解法有時序風險 — observer 是非同步、<code>isApplying</code> 可能在錯時間被讀。<strong>disconnect/observe 配對更穩</strong>。</p>
<h3 id="解法替代root-與目標分離">解法替代：root 與目標分離</h3>
<p>如果 apply 改的是 A、observer 監聽的是 B（A 跟 B 沒交集），自然不循環：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">resultsEl</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="c1">// 改的是 input 而不是 results — 不會觸發 observer
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">inputEl</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s1">&#39;...&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>設計時讓 observer 看的範圍跟 apply 改的範圍<strong>結構上分離</strong> — 是最乾淨的解法、不需要 disconnect 配對。</p>
<hr>
<h2 id="觀察的時機問題">觀察的時機問題</h2>
<h3 id="observer-跟-framework-渲染週期競爭">Observer 跟 framework 渲染週期競爭</h3>
<p>Observer 在 framework 連續 patch 中段觸發、可能在 layout 還沒穩時就跑 apply、造成短暫視覺錯位：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// framework 連續 patch：
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">//   patch 1 → observer 觸發 → apply 跑 → 視覺 A
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">//   patch 2 → observer 觸發 → apply 跑 → 視覺 B
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">//   patch 3 → observer 觸發 → apply 跑 → 視覺 C（最終）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 使用者看到 A → B → C 的閃爍
</span></span></span></code></pre></div><p>Debounce 是這個問題的解 — 讓 observer 等 patch 完成才跑 apply。</p>
<h3 id="確認時機正確">確認時機正確</h3>
<p>寫 observer 時自問：</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>答案決定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Apply 跑的時候 layout 是否已穩定？</td>
          <td>是否需要 debounce</td>
      </tr>
      <tr>
          <td>Apply 自己改 DOM 嗎？</td>
          <td>是否需要 disconnect 配對</td>
      </tr>
      <tr>
          <td>我關心的變動類型是什麼？</td>
          <td>option flag 怎麼勾</td>
      </tr>
      <tr>
          <td>變動發生在哪一層？</td>
          <td>是否需要 subtree</td>
      </tr>
      <tr>
          <td>Framework 的渲染週期會干擾嗎？</td>
          <td>debounce 時間取多久</td>
      </tr>
  </tbody>
</table>
<p>每個問題都該有顯式答案、不能丟預設。</p>
<hr>
<h2 id="內在屬性比較四種-observer-設計">內在屬性比較：四種 observer 設計</h2>
<table>
  <thead>
      <tr>
          <th>設計</th>
          <th>觸發頻率</th>
          <th>Layout 穩定性</th>
          <th>維護成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>全勾 + subtree + 無 debounce</td>
          <td>最高</td>
          <td>低 — patch 中段觸發</td>
          <td>低（短期）/ 高（debug 噩夢）</td>
      </tr>
      <tr>
          <td>收斂 root + 必要 option + 無 debounce</td>
          <td>中</td>
          <td>中</td>
          <td>中</td>
      </tr>
      <tr>
          <td>收斂 root + 必要 option + debounce</td>
          <td>低</td>
          <td>高</td>
          <td>中</td>
      </tr>
      <tr>
          <td>結構分離 + 收斂 + debounce</td>
          <td>最低</td>
          <td>最高</td>
          <td>中（前期設計成本）</td>
      </tr>
  </tbody>
</table>
<p><strong>推薦</strong>：收斂 root + 必要 option + debounce。<code>apply</code> 不改 DOM 時不需要 disconnect；改的話用結構分離優先、退而求其次用 disconnect。</p>
<hr>
<h2 id="進階技巧">進階技巧</h2>
<h3 id="1-動態調整-observer-範圍">1. 動態調整 observer 範圍</h3>
<p>當監聽目標可能還沒 mount 時、用兩階段 observer：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 階段 1：等目標 mount
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">bootstrap</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">target</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__results&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">target</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">bootstrap</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">// 階段 2：mount 後監聽目標
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">target</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">bootstrap</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>階段 1 用寬範圍找到目標、階段 2 切到精準範圍 — 把寬範圍的觸發限制在「找目標」這個短時間。</p>
<h3 id="2-用-takerecords-主動取出累積變動">2. 用 <code>takeRecords</code> 主動取出累積變動</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">observer</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 之後某時間點、想立刻處理累積的變動
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">records</span> <span class="o">=</span> <span class="nx">observer</span><span class="p">.</span><span class="nx">takeRecords</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">processRecords</span><span class="p">(</span><span class="nx">records</span><span class="p">);</span></span></span></code></pre></div><p><code>takeRecords</code> 取出尚未觸發回呼的變動記錄、主動處理 — 適合「我想在某時間點同步處理累積變動」場景。</p>
<h3 id="3-多-observer-各管一塊">3. 多 observer 各管一塊</h3>
<p>不要用一個 observer 監聽全部、各分一個：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">applyA</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">elA</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">applyB</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">elB</span><span class="p">,</span> <span class="p">{</span> <span class="nx">attributes</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>各自獨立 — 一個 observer 出錯不影響另一個、debug 範圍小、option 各自最佳化。</p>
<hr>
<h2 id="設計取捨mutationobserver-的設計策略">設計取捨：MutationObserver 的設計策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（收斂 root + 必要 option + debounce）當預設、其他做法在特定情境合理。</p>
<h3 id="a收斂-root--必要-option--debounce--結構分離這個專案的預設">A：收斂 root + 必要 option + debounce + 結構分離（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：root 取最小共同 ancestor、option 只勾真正關心的變動、加 50-100ms debounce、apply 改的範圍跟 observer 看的範圍結構上分離</li>
<li><strong>選 A 的理由</strong>：觸發頻率最低、layout 穩定、無 self-mutation 循環風險</li>
<li><strong>適合</strong>：絕大多數 observer 設計</li>
<li><strong>代價</strong>：前期設計成本中（要思考 root / option / 結構）</li>
</ul>
<h3 id="b收斂-root--必要-option無-debounce">B：收斂 root + 必要 option（無 debounce）</h3>
<ul>
<li><strong>機制</strong>：縮範圍與 option、但不加 debounce</li>
<li><strong>跟 A 的取捨</strong>：B 即時反應、A 等 debounce；但 B 在 framework patch 中段觸發、layout 不穩時跑 apply 結果不可靠</li>
<li><strong>B 比 A 好的情境</strong>：apply 不依賴 layout（純改 attribute、不讀 bounding rect）</li>
</ul>
<h3 id="c寬範圍--subtree--全勾-option預設配置">C：寬範圍 + subtree + 全勾 option（預設配置）</h3>
<ul>
<li><strong>機制</strong>：observe(elem, { childList: true, subtree: true, attributes: true, &hellip;})</li>
<li><strong>C 是反模式</strong>：「以防萬一全勾」會觸發數十倍頻率的 callback、framework 環境必撞效能 / 競態 bug</li>
<li><strong>看起來吸引人的原因</strong>：寫法簡單、不用想要監聽什麼、「全部都看就不會漏」</li>
<li><strong>實際發生的代價</strong>：CPU 100%、layout thrashing、self-mutation 引發無限迴圈</li>
</ul>
<h3 id="ddisconnect--observe-配對處理-self-mutation">D：disconnect / observe 配對處理 self-mutation</h3>
<ul>
<li><strong>機制</strong>：apply 前 disconnect、apply 後 reconnect</li>
<li><strong>跟 A（結構分離）的取捨</strong>：D 處理 callback 必須改 observer 監聽範圍的情境、A 從設計上避免；A 更乾淨</li>
<li><strong>D 比 A 好的情境</strong>：無法做結構分離（apply 必須改 observer 看的範圍）— 唯一情境</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Observer 問題</th>
          <th>修正動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>短時間觸發數十次</td>
          <td>範圍 / option 太寬</td>
          <td>縮 root、移除不需要的 option、加 debounce</td>
      </tr>
      <tr>
          <td>Apply 跑時 layout 抖動</td>
          <td>在 framework patch 中段觸發</td>
          <td>加 debounce 50-100ms</td>
      </tr>
      <tr>
          <td>Apply 內改 DOM 進入無限循環</td>
          <td>沒處理 self-mutation</td>
          <td>用結構分離 / disconnect 配對</td>
      </tr>
      <tr>
          <td>預期變動沒觸發</td>
          <td>option 沒勾對、root 選錯</td>
          <td>對照變動類型確認 option</td>
      </tr>
      <tr>
          <td>Subtree 用了但只關心直接子</td>
          <td>過度監聽深度</td>
          <td>移除 subtree、改用直接子監聽</td>
      </tr>
      <tr>
          <td>屬性監聽觸發太頻繁</td>
          <td>沒用 attributeFilter</td>
          <td>加 filter 限縮屬性</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：MutationObserver 是非同步監聽、跟同步 selector 設計工具完全不同。範圍 / option / 頻率三維度都要顯式設計 — 預設組合會在 framework 環境中過度觸發、且難以 debug。</p>
<p><code>subtree: true</code> + <code>attributes: true</code> 是「監聽全部」的便利、窄 root + 最少 option 是「精準監聽」的對齊 — 同 <a href="../minimum-necessary-scope-is-sanity-defense/">#43 最小必要範圍</a> 跟 <a href="../ease-of-writing-vs-intent-alignment/">#67 便利 vs 對齊反相關</a>。</p>
]]></content:encoded></item><item><title>Pattern：Document 全文件 query</title><link>https://tarrragon.github.io/blog/report/pattern-document-query/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-document-query/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>從整個頁面找元素、不指定 ancestor scope。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>簡潔。一行就能取到目標、不需要先建立元件根變數。在「我只想快速確認某個元素在不在 / 取它的某個屬性」這類情境下、寫一個 import shell 變數 + null check 是過度工程。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Devtools console 一行查詢&lt;/td>
 &lt;td>沒有「未來會壞」的問題、用完就丟&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>原型 / spike 階段程式碼&lt;/td>
 &lt;td>預期會被丟棄重寫、不需要長期維護考量&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>確定全頁唯一的單例（&lt;code>document.body&lt;/code>、&lt;code>&amp;lt;html&amp;gt;&lt;/code>）&lt;/td>
 &lt;td>從定義上不會多個、也不會被誤命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Build-time script、不會在 runtime 跑&lt;/td>
 &lt;td>沒有「同頁多元件」的可能性&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>核心特徵：&lt;strong>這段程式不會在多元件 / 動態 DOM 環境長期存活&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>失敗模式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Production 客製、預期長期存活&lt;/td>
 &lt;td>未來頁面結構變動、誤命中或漏命中&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同頁可能有多個同類元件&lt;/td>
 &lt;td>只取第一個、其他被忽略且不報錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件可能在 SPA 路由中動態增減&lt;/td>
 &lt;td>query 時機跟元件 mount 時機不對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫入第三方函式庫&lt;/td>
 &lt;td>使用者頁面的其他 class 可能跟你的 selector 撞&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>安靜失敗是最危險的特徵&lt;/strong> — 不報錯、操作了錯元素、bug 表現遠離 root cause。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法、document query 是其中之一：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>做法&lt;/th>
 &lt;th>比較&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>本卡片：document query&lt;/td>
 &lt;td>簡潔但不防護未來變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>多一行 setup、換到「shell 內隔離」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>多實例支援、適合可能擴展的客製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>事件委派情境、動態元件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>選擇順序：production 客製預設用「元件根變數」、原型 / 探索 / 一次性才用 document query。&lt;/p>
&lt;hr>
&lt;h2 id="邊界什麼時候-document-query-在-production-也合理">邊界：什麼時候 document query 在 production 也合理&lt;/h2>
&lt;p>幾個常見的 production 例外：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 1：操作的目標就是「全頁面唯一單例」
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;page-search&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">documentElement&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-theme&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;dark&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 2：跨元件邊界的元素（不在任何元件內）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">slot&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-filter-slot&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">// (slot 是 main 的子節點、不在 search-shell 內、不能從 shell 找)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">// 例外 3：頁面層級的 meta 元素
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;meta[name=&amp;#34;description&amp;#34;]&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例外都共享一個特徵：&lt;strong>目標元素本質上就在「頁面層級」、不是任何元件的內部&lt;/strong>。&lt;/p>
&lt;p>不是例外的場景、即使「當前頁面只有一個」、也用元件根變數 — 預防未來擴展。&lt;/p>
&lt;hr>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該換做法嗎？&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>「現在只有一個、之後再想」&lt;/td>
 &lt;td>是 — 換元件根變數&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同檔案多處 &lt;code>document.querySelector('.x')&lt;/code>&lt;/td>
 &lt;td>是 — 至少改成存變數重用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫第三方 library 用 document query&lt;/td>
 &lt;td>是 — 改用根參數 pattern&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作 &lt;code>document.body&lt;/code> / &lt;code>&amp;lt;html&amp;gt;&lt;/code>&lt;/td>
 &lt;td>否 — 這就是合理場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式跑一次後丟棄（migration script）&lt;/td>
 &lt;td>否 — 簡潔優先&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心原則&lt;/strong>：document query 不是反模式、是有適用範圍的工具。判斷「這段程式預期活多久」 — 短命用 document、長命用元件根。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span></span></span></code></pre></div><p>從整個頁面找元素、不指定 ancestor scope。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>簡潔。一行就能取到目標、不需要先建立元件根變數。在「我只想快速確認某個元素在不在 / 取它的某個屬性」這類情境下、寫一個 import shell 變數 + null check 是過度工程。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Devtools console 一行查詢</td>
          <td>沒有「未來會壞」的問題、用完就丟</td>
      </tr>
      <tr>
          <td>原型 / spike 階段程式碼</td>
          <td>預期會被丟棄重寫、不需要長期維護考量</td>
      </tr>
      <tr>
          <td>確定全頁唯一的單例（<code>document.body</code>、<code>&lt;html&gt;</code>）</td>
          <td>從定義上不會多個、也不會被誤命中</td>
      </tr>
      <tr>
          <td>Build-time script、不會在 runtime 跑</td>
          <td>沒有「同頁多元件」的可能性</td>
      </tr>
  </tbody>
</table>
<p>核心特徵：<strong>這段程式不會在多元件 / 動態 DOM 環境長期存活</strong>。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>失敗模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production 客製、預期長期存活</td>
          <td>未來頁面結構變動、誤命中或漏命中</td>
      </tr>
      <tr>
          <td>同頁可能有多個同類元件</td>
          <td>只取第一個、其他被忽略且不報錯</td>
      </tr>
      <tr>
          <td>元件可能在 SPA 路由中動態增減</td>
          <td>query 時機跟元件 mount 時機不對齊</td>
      </tr>
      <tr>
          <td>寫入第三方函式庫</td>
          <td>使用者頁面的其他 class 可能跟你的 selector 撞</td>
      </tr>
  </tbody>
</table>
<p><strong>安靜失敗是最危險的特徵</strong> — 不報錯、操作了錯元素、bug 表現遠離 root cause。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法、document query 是其中之一：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本卡片：document query</td>
          <td>簡潔但不防護未來變動</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>多一行 setup、換到「shell 內隔離」</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>多實例支援、適合可能擴展的客製</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>事件委派情境、動態元件</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：production 客製預設用「元件根變數」、原型 / 探索 / 一次性才用 document query。</p>
<hr>
<h2 id="邊界什麼時候-document-query-在-production-也合理">邊界：什麼時候 document query 在 production 也合理</h2>
<p>幾個常見的 production 例外：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 例外 1：操作的目標就是「全頁面唯一單例」
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;page-search&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-theme&#39;</span><span class="p">,</span> <span class="s1">&#39;dark&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">// 例外 2：跨元件邊界的元素（不在任何元件內）
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">slot</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">// (slot 是 main 的子節點、不在 search-shell 內、不能從 shell 找)
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1">// 例外 3：頁面層級的 meta 元素
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;meta[name=&#34;description&#34;]&#39;</span><span class="p">);</span></span></span></code></pre></div><p>例外都共享一個特徵：<strong>目標元素本質上就在「頁面層級」、不是任何元件的內部</strong>。</p>
<p>不是例外的場景、即使「當前頁面只有一個」、也用元件根變數 — 預防未來擴展。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換做法嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>「現在只有一個、之後再想」</td>
          <td>是 — 換元件根變數</td>
      </tr>
      <tr>
          <td>同檔案多處 <code>document.querySelector('.x')</code></td>
          <td>是 — 至少改成存變數重用</td>
      </tr>
      <tr>
          <td>寫第三方 library 用 document query</td>
          <td>是 — 改用根參數 pattern</td>
      </tr>
      <tr>
          <td>操作 <code>document.body</code> / <code>&lt;html&gt;</code></td>
          <td>否 — 這就是合理場景</td>
      </tr>
      <tr>
          <td>程式跑一次後丟棄（migration script）</td>
          <td>否 — 簡潔優先</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：document query 不是反模式、是有適用範圍的工具。判斷「這段程式預期活多久」 — 短命用 document、長命用元件根。</p>
]]></content:encoded></item><item><title>Pattern：元件根變數 query</title><link>https://tarrragon.github.io/blog/report/pattern-component-root/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-component-root/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">// ... 之後所有 query 都從 shell 開始
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把元件根 query 一次存變數、所有後續 query 都從這個變數開始。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把 selector 的作用範圍從「全頁面」收斂到「元件內部」。即使未來頁面其他地方出現同名元素、跟我無關。成本只多一行 query + 一個 null check、防護收益大。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Production 客製、預期長期存活&lt;/td>
 &lt;td>未來頁面結構可能變動、需要隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>當前只有一個元件實例、未來可能加&lt;/td>
 &lt;td>提早預防、改造成本最低&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件根 mount 後不會被移除&lt;/td>
 &lt;td>變數生命週期跟元件一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式跑在頁面 mount 後（DOMContentLoaded 後）&lt;/td>
 &lt;td>shell 可被找到&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：寫的時候只有一個元件、但希望程式碼能容忍未來頁面結構變動。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼不夠&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>同頁同時有多個元件實例&lt;/td>
 &lt;td>變數只存第一個 shell、其他被忽略&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件動態增減（SPA 路由切換）&lt;/td>
 &lt;td>變數指向 stale DOM&lt;/td>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次性 / 探索期程式&lt;/td>
 &lt;td>過度工程&lt;/td>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="null-check-的時機">Null check 的時機&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>頁面可能沒有 shell（不是搜尋頁），所有後續 query 都會 null pointer。提早 return 比後續一連串 &lt;code>if (drawer)&lt;/code> 乾淨。&lt;/p>
&lt;p>&lt;strong>等同於宣告&lt;/strong>：「這段程式只在有 shell 的頁面執行」。&lt;/p>
&lt;h3 id="變數的宣告位置">變數的宣告位置&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>位置&lt;/th>
 &lt;th>適合&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>函式內 local 變數&lt;/td>
 &lt;td>預設、scope 最小&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Module scope（IIFE 內）&lt;/td>
 &lt;td>多函式共用同一 shell&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Class instance property&lt;/td>
 &lt;td>元件本身用 class 包裝時&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>避免全域變數 — &lt;code>window.shell&lt;/code> 容易跟其他 script 撞。&lt;/p>
&lt;h3 id="等待-shell-mount-的處理">等待 shell mount 的處理&lt;/h3>
&lt;p>如果 script 跑得太早（shell 還沒 mount），shell 會是 null：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 解法 1：等 DOMContentLoaded
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 解法 2：MutationObserver 等 mount
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">bootstrap&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">bootstrap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">disconnect&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="nx">bootstrap&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">subtree&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>選擇取決於 shell 是 server-render 還是 client-render&lt;/strong>：server-render 用 DOMContentLoaded、client-render 用 observer。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>做法&lt;/th>
 &lt;th>比較&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>比本卡片簡潔、不防護未來變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：元件根變數&lt;/td>
 &lt;td>多一行設定、隔離未來頁面變動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>比本卡片多支援多實例、設計成本前移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>適合動態元件、不依賴變數綁定的時間&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>預設用本卡片、需要多實例升級到「起點當參數」、需要動態升級到「closest」。&lt;/p>
&lt;hr>
&lt;h2 id="應用範例完整-setup">應用範例：完整 setup&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">input&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">drawer&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 元件未完整 mount
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupFilterSlotSwap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">input&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;DOMContentLoaded&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">init&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>shell 取一次、各 setup 函式從 shell 派生需要的子節點。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// ... 之後所有 query 都從 shell 開始
</span></span></span></code></pre></div><p>把元件根 query 一次存變數、所有後續 query 都從這個變數開始。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把 selector 的作用範圍從「全頁面」收斂到「元件內部」。即使未來頁面其他地方出現同名元素、跟我無關。成本只多一行 query + 一個 null check、防護收益大。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production 客製、預期長期存活</td>
          <td>未來頁面結構可能變動、需要隔離</td>
      </tr>
      <tr>
          <td>當前只有一個元件實例、未來可能加</td>
          <td>提早預防、改造成本最低</td>
      </tr>
      <tr>
          <td>元件根 mount 後不會被移除</td>
          <td>變數生命週期跟元件一致</td>
      </tr>
      <tr>
          <td>程式跑在頁面 mount 後（DOMContentLoaded 後）</td>
          <td>shell 可被找到</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：寫的時候只有一個元件、但希望程式碼能容忍未來頁面結構變動。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁同時有多個元件實例</td>
          <td>變數只存第一個 shell、其他被忽略</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
      </tr>
      <tr>
          <td>元件動態增減（SPA 路由切換）</td>
          <td>變數指向 stale DOM</td>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
      </tr>
      <tr>
          <td>一次性 / 探索期程式</td>
          <td>過度工程</td>
          <td><a href="../pattern-document-query/">document query</a></td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="null-check-的時機">Null check 的時機</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span></span></span></code></pre></div><p>頁面可能沒有 shell（不是搜尋頁），所有後續 query 都會 null pointer。提早 return 比後續一連串 <code>if (drawer)</code> 乾淨。</p>
<p><strong>等同於宣告</strong>：「這段程式只在有 shell 的頁面執行」。</p>
<h3 id="變數的宣告位置">變數的宣告位置</h3>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>函式內 local 變數</td>
          <td>預設、scope 最小</td>
      </tr>
      <tr>
          <td>Module scope（IIFE 內）</td>
          <td>多函式共用同一 shell</td>
      </tr>
      <tr>
          <td>Class instance property</td>
          <td>元件本身用 class 包裝時</td>
      </tr>
  </tbody>
</table>
<p>避免全域變數 — <code>window.shell</code> 容易跟其他 script 撞。</p>
<h3 id="等待-shell-mount-的處理">等待 shell mount 的處理</h3>
<p>如果 script 跑得太早（shell 還沒 mount），shell 會是 null：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 解法 1：等 DOMContentLoaded
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 解法 2：MutationObserver 等 mount
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">bootstrap</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">bootstrap</span><span class="p">.</span><span class="nx">disconnect</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">bootstrap</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p><strong>選擇取決於 shell 是 server-render 還是 client-render</strong>：server-render 用 DOMContentLoaded、client-render 用 observer。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>比本卡片簡潔、不防護未來變動</td>
      </tr>
      <tr>
          <td>本卡片：元件根變數</td>
          <td>多一行設定、隔離未來頁面變動</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>比本卡片多支援多實例、設計成本前移</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>適合動態元件、不依賴變數綁定的時間</td>
      </tr>
  </tbody>
</table>
<p>預設用本卡片、需要多實例升級到「起點當參數」、需要動態升級到「closest」。</p>
<hr>
<h2 id="應用範例完整-setup">應用範例：完整 setup</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">var</span> <span class="nx">ui</span>     <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">input</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 元件未完整 mount
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-scope&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="nx">setupFilterSlotSwap</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">input</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;DOMContentLoaded&#39;</span><span class="p">,</span> <span class="nx">init</span><span class="p">);</span></span></span></code></pre></div><p>shell 取一次、各 setup 函式從 shell 派生需要的子節點。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該換做法嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多函式共用同一 shell、各自重 query</td>
          <td>否 — 把 shell 提到 module scope 共用</td>
      </tr>
      <tr>
          <td>同頁面要支援多個 shell 實例</td>
          <td>是 — 升級到「起點當參數」</td>
      </tr>
      <tr>
          <td>元件可能在 runtime 動態出現 / 消失</td>
          <td>是 — 升級到「closest 反向」</td>
      </tr>
      <tr>
          <td>Shell 偶爾找不到（時序問題）</td>
          <td>否 — 加 MutationObserver bootstrap、做法不變</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：本 pattern 是 production 客製的預設、不是極致最佳化。當當前情境不複雜（一個元件、靜態 mount）、用本 pattern 即可；情境變複雜時再升級到對應做法。</p>
]]></content:encoded></item><item><title>Pattern：起點當函式參數</title><link>https://tarrragon.github.io/blog/report/pattern-root-as-parameter/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-root-as-parameter/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">ui&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">drawer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 所有 query 從參數 shell 開始
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>元件根不在函式內 query、由呼叫者傳入。函式支援任意數量的元件實例。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>兩件事：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>多實例支援免費&lt;/strong>：&lt;code>forEach(setup)&lt;/code> 自動處理多個 shell&lt;/li>
&lt;li>&lt;strong>純函式特性&lt;/strong>：函式行為只依賴參數、不依賴外部狀態 — 可單獨測試、可重用、副作用集中&lt;/li>
&lt;/ol>
&lt;p>跟&lt;a href="../pattern-component-root/">元件根變數&lt;/a>的關鍵差異：那個 pattern 假設「shell 唯一」、本 pattern 把這個假設外移到呼叫端、函式本身不假設。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>同頁面有多個元件實例（多語切換、相關搜尋）&lt;/td>
 &lt;td>&lt;code>forEach&lt;/code> 自動覆蓋全部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件設計成可被重用到其他頁面&lt;/td>
 &lt;td>沒有 hardcoded 依賴、容易移植&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>寫成函式庫 / 第三方 component&lt;/td>
 &lt;td>使用者可以對任意根節點呼叫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>想單元測試函式行為&lt;/td>
 &lt;td>傳入 mock root 即可測試&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：把「shell 從哪來」這個責任明確交給呼叫端、函式自己不關心。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼過度工程&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>確定全站只有一個元件實例&lt;/td>
 &lt;td>每函式多一個參數、收益不明顯&lt;/td>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件動態增減、生命週期不可預測&lt;/td>
 &lt;td>forEach 只跑一次、無法捕捉後加的元件&lt;/td>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一次性探索程式碼&lt;/td>
 &lt;td>純函式設計成本不值得&lt;/td>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="函式簽名的設計">函式簽名的設計&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 好：shell 是必填參數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="p">...&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 較差：依賴外部變數
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// module scope
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 用了外部 shell
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// 更差：mega object
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">allElements&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">allElements&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 不知道實際依賴什麼
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>明確參數 = 明確依賴 = 容易測試、容易讀。&lt;/p>
&lt;h3 id="內部子函式也接受-shell">內部子函式也接受 shell&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupFilterSlot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">setupScopeFilter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每層都明確接受 shell — 不依賴外層 closure。整套函式族都是純函式。&lt;/p>
&lt;h3 id="預先抓子節點-vs-每次重-query">預先抓子節點 vs 每次重 query&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 方式 A：函式入口抓所有子節點
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">els&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">ui&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="nx">input&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__drawer&amp;#39;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 後續用 els.ui / els.input / els.drawer
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">// 方式 B：各子函式自己 query
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupSearchShell&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">syncScopeHeight&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 內部自己 querySelector
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">setupFilterSlot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A 比較有效率（只 query 一次）、B 比較解耦（子函式自包含）。&lt;strong>選 B 為預設、效能瓶頸時才考慮 A&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>做法&lt;/th>
 &lt;th>比較&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>比本卡片簡潔、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>比本卡片少一個參數、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：起點當參數&lt;/td>
 &lt;td>多實例支援、純函式、設計成本前移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-closest-lookup/">closest 反向找根&lt;/a>&lt;/td>
 &lt;td>比本卡片更動態、不依賴 forEach 時機&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>升級階梯：document → 元件根變數 → 起點當參數 → closest。複雜度遞增、能處理的情境也遞增。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">ui</span>     <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">var</span> <span class="nx">input</span>  <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 所有 query 從參數 shell 開始
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span></span></span></code></pre></div><p>元件根不在函式內 query、由呼叫者傳入。函式支援任意數量的元件實例。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>兩件事：</p>
<ol>
<li><strong>多實例支援免費</strong>：<code>forEach(setup)</code> 自動處理多個 shell</li>
<li><strong>純函式特性</strong>：函式行為只依賴參數、不依賴外部狀態 — 可單獨測試、可重用、副作用集中</li>
</ol>
<p>跟<a href="../pattern-component-root/">元件根變數</a>的關鍵差異：那個 pattern 假設「shell 唯一」、本 pattern 把這個假設外移到呼叫端、函式本身不假設。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁面有多個元件實例（多語切換、相關搜尋）</td>
          <td><code>forEach</code> 自動覆蓋全部</td>
      </tr>
      <tr>
          <td>元件設計成可被重用到其他頁面</td>
          <td>沒有 hardcoded 依賴、容易移植</td>
      </tr>
      <tr>
          <td>寫成函式庫 / 第三方 component</td>
          <td>使用者可以對任意根節點呼叫</td>
      </tr>
      <tr>
          <td>想單元測試函式行為</td>
          <td>傳入 mock root 即可測試</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：把「shell 從哪來」這個責任明確交給呼叫端、函式自己不關心。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼過度工程</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>確定全站只有一個元件實例</td>
          <td>每函式多一個參數、收益不明顯</td>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
      </tr>
      <tr>
          <td>元件動態增減、生命週期不可預測</td>
          <td>forEach 只跑一次、無法捕捉後加的元件</td>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
      </tr>
      <tr>
          <td>一次性探索程式碼</td>
          <td>純函式設計成本不值得</td>
          <td><a href="../pattern-document-query/">document query</a></td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="函式簽名的設計">函式簽名的設計</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 好：shell 是必填參數
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 較差：依賴外部變數
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">shell</span><span class="p">;</span>  <span class="c1">// module scope
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">// 用了外部 shell
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// 更差：mega object
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">allElements</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">allElements</span><span class="p">.</span><span class="nx">shell</span><span class="p">;</span>  <span class="c1">// 不知道實際依賴什麼
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>明確參數 = 明確依賴 = 容易測試、容易讀。</p>
<h3 id="內部子函式也接受-shell">內部子函式也接受 shell</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nx">setupFilterSlot</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">setupScopeFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">function</span> <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">var</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-scope&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div><p>每層都明確接受 shell — 不依賴外層 closure。整套函式族都是純函式。</p>
<h3 id="預先抓子節點-vs-每次重-query">預先抓子節點 vs 每次重 query</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 方式 A：函式入口抓所有子節點
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">els</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">ui</span><span class="o">:</span>     <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">input</span><span class="o">:</span>  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">drawer</span><span class="o">:</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">// 後續用 els.ui / els.input / els.drawer
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// 方式 B：各子函式自己 query
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">syncScopeHeight</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>  <span class="c1">// 內部自己 querySelector
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span>  <span class="nx">setupFilterSlot</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>A 比較有效率（只 query 一次）、B 比較解耦（子函式自包含）。<strong>選 B 為預設、效能瓶頸時才考慮 A</strong>。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>比本卡片簡潔、無多實例支援</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>比本卡片少一個參數、無多實例支援</td>
      </tr>
      <tr>
          <td>本卡片：起點當參數</td>
          <td>多實例支援、純函式、設計成本前移</td>
      </tr>
      <tr>
          <td><a href="../pattern-closest-lookup/">closest 反向找根</a></td>
          <td>比本卡片更動態、不依賴 forEach 時機</td>
      </tr>
  </tbody>
</table>
<p>升級階梯：document → 元件根變數 → 起點當參數 → closest。複雜度遞增、能處理的情境也遞增。</p>
<hr>
<h2 id="應用範例多實例-setup">應用範例：多實例 setup</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 頁面有 N 個 search-shell（例如多語版面切換）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 跑完之後：每個 shell 各自獨立 setup、互不干擾
</span></span></span></code></pre></div><p>當前頁只一個 shell、上面這行也適用 —<code>forEach</code> 對 1 個元素跑一次、跟 hardcode 單例沒差。<strong>做了多實例設計、未來不需要重寫</strong>。</p>
<hr>
<h2 id="應用範例單元測試">應用範例：單元測試</h2>
<p>純函式可以對 mock DOM 測試：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">test</span><span class="p">(</span><span class="s1">&#39;setupSearchShell 把 filter 移到 sidebar&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">createMockShell</span><span class="p">();</span>  <span class="c1">// 建立測試用 DOM
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">).</span><span class="nx">children</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>不需要全頁面 mount、只需要 mock 一個 shell — 測試成本低。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同頁要支援多個元件實例</td>
          <td>是 — 直接的好處</td>
      </tr>
      <tr>
          <td>想對函式寫單元測試</td>
          <td>是 — 純函式才好測</td>
      </tr>
      <tr>
          <td>函式內讀 module scope 變數</td>
          <td>是 — 改成參數讓依賴顯式</td>
      </tr>
      <tr>
          <td>確定永遠只一個實例、且不寫測試</td>
          <td>否 — <a href="../pattern-component-root/">元件根變數</a> 已夠</td>
      </tr>
      <tr>
          <td>元件實例 runtime 動態增減</td>
          <td>否 — 升級到 <a href="../pattern-closest-lookup/">closest</a></td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：本 pattern 把「我從哪取得 shell」的答案從函式內搬到呼叫端 — 換到「函式可重用」+「測試容易」+「多實例免費」三個收益、代價是函式簽名多一個參數。當前情境只一個實例也適用、未來擴展不需重寫。</p>
]]></content:encoded></item><item><title>Pattern：closest 反向找根</title><link>https://tarrragon.github.io/blog/report/pattern-closest-lookup/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-closest-lookup/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 在這個 shell 內處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">handleSearchClick&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不在初始化時綁定 listener、而是頁面層級委派事件、事件處理時從 &lt;code>e.target&lt;/code> 反向找元件根。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把「找元件根」從「初始化時綁定」延後到「事件發生時動態判斷」 — 換到三個能力：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>元件動態增減免處理&lt;/strong>：新加的元件不需要重新綁 listener&lt;/li>
&lt;li>&lt;strong>多實例不需要 forEach setup&lt;/strong>：所有實例共用一個 listener&lt;/li>
&lt;li>&lt;strong>記憶體效率&lt;/strong>：N 個元件只綁 1 個 listener、不是 N 個&lt;/li>
&lt;/ol>
&lt;p>代價是事件處理邏輯多一層（每次都要 closest 反向找）。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SPA 路由切換、元件動態 mount/unmount&lt;/td>
 &lt;td>不需要在 mount 時重綁 listener&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件數量大（&amp;gt;10 個實例）&lt;/td>
 &lt;td>事件委派比每實例綁 listener 省記憶體&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元件透過 AJAX 動態注入&lt;/td>
 &lt;td>注入後不需要任何 setup 動作&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第三方 widget、不能控制元件生命週期&lt;/td>
 &lt;td>listener 綁在 document、跟 widget 解耦&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：元件的 mount 時機 / 數量 runtime 才知道、不是初始化時固定。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼過度工程&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>元件靜態 mount、生命週期跟頁面一樣&lt;/td>
 &lt;td>委派多一層、收益不明顯&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一個元件實例、永不變動&lt;/td>
 &lt;td>完全沒必要&lt;/td>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要在元件 mount 時就跑邏輯（不只回應事件）&lt;/td>
 &lt;td>closest 只在事件發生時跑、無法當 init hook&lt;/td>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a> + MutationObserver&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="closest-失敗的處理">Closest 失敗的處理&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 點擊不在任何 shell 內
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>closest&lt;/code> 找不到時回 &lt;code>null&lt;/code>、提早 return 是必要防護。&lt;strong>沒這個 check 會在頁面其他地方點擊時報錯&lt;/strong>。&lt;/p>
&lt;h3 id="從-closest-結果再往下-query">從 closest 結果再往下 query&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">input&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__search-input&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>closest&lt;/code> 找到 shell 後、可以從 shell 往下 query 同元件內的其他元素 — 這是「事件 + closest + 局部 query」的組合。&lt;/p>
&lt;h3 id="事件類型的選擇">事件類型的選擇&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>事件&lt;/th>
 &lt;th>適合&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>click&lt;/code>&lt;/td>
 &lt;td>點擊互動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>input&lt;/code>&lt;/td>
 &lt;td>輸入框文字變動（需要 bubble）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>change&lt;/code>&lt;/td>
 &lt;td>選項變動（select / radio / checkbox）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>keydown&lt;/code>&lt;/td>
 &lt;td>鍵盤快捷鍵&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>focus&lt;/code> / &lt;code>blur&lt;/code>&lt;/td>
 &lt;td>焦點移動（不 bubble、要用 &lt;code>focusin&lt;/code> / &lt;code>focusout&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>注意 &lt;code>focus&lt;/code> / &lt;code>blur&lt;/code> 不會 bubble — 事件委派要用 &lt;code>focusin&lt;/code> / &lt;code>focusout&lt;/code>。&lt;/p>
&lt;h3 id="委派的根節點選擇">委派的根節點選擇&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 選項 1：document（最寬）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handler&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 選項 2：特定容器（縮範圍）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">pageContainer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;main&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">pageContainer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;click&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">handler&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>縮範圍的好處是「跟其他頁面區域的 listener 不互相干擾」。預設用 document、有干擾風險才縮。&lt;/p>
&lt;hr>
&lt;h2 id="跟其他起點做法的關係">跟其他起點做法的關係&lt;/h2>
&lt;p>&lt;a href="../dom-selector-precision/">#14 Selector 精準度&lt;/a> 的「起點」維度有四種做法：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>做法&lt;/th>
 &lt;th>比較&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-document-query/">document query&lt;/a>&lt;/td>
 &lt;td>靜態、簡潔、無多實例支援&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-component-root/">元件根變數&lt;/a>&lt;/td>
 &lt;td>靜態、shell 唯一假設&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="../pattern-root-as-parameter/">起點當參數&lt;/a>&lt;/td>
 &lt;td>靜態多實例、forEach 一次設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本卡片：closest 反向找根&lt;/td>
 &lt;td>動態、事件驅動、無 init 時機綁定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>複雜度遞增、能處理的動態程度也遞增。最動態的場景才用本 pattern。&lt;/p>
&lt;hr>
&lt;h2 id="應用範例跨多-shell-的-scope-filter">應用範例：跨多 shell 的 scope filter&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">setupGlobalScopeFilter&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">e&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">shell&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-shell&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">scope&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">target&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">closest&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">scope&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 不是 scope 控制的 change
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">applyScope&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">shell&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">scope&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="nx">setupGlobalScopeFilter&lt;/span>&lt;span class="p">();&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一個 listener 處理所有 shell 的 scope 變動 — 不論 shell 是初始 mount 的、還是 runtime 注入的。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="c1">// 在這個 shell 內處理
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>  <span class="nx">handleSearchClick</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">e</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>不在初始化時綁定 listener、而是頁面層級委派事件、事件處理時從 <code>e.target</code> 反向找元件根。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把「找元件根」從「初始化時綁定」延後到「事件發生時動態判斷」 — 換到三個能力：</p>
<ol>
<li><strong>元件動態增減免處理</strong>：新加的元件不需要重新綁 listener</li>
<li><strong>多實例不需要 forEach setup</strong>：所有實例共用一個 listener</li>
<li><strong>記憶體效率</strong>：N 個元件只綁 1 個 listener、不是 N 個</li>
</ol>
<p>代價是事件處理邏輯多一層（每次都要 closest 反向找）。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SPA 路由切換、元件動態 mount/unmount</td>
          <td>不需要在 mount 時重綁 listener</td>
      </tr>
      <tr>
          <td>元件數量大（&gt;10 個實例）</td>
          <td>事件委派比每實例綁 listener 省記憶體</td>
      </tr>
      <tr>
          <td>元件透過 AJAX 動態注入</td>
          <td>注入後不需要任何 setup 動作</td>
      </tr>
      <tr>
          <td>第三方 widget、不能控制元件生命週期</td>
          <td>listener 綁在 document、跟 widget 解耦</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：元件的 mount 時機 / 數量 runtime 才知道、不是初始化時固定。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼過度工程</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>元件靜態 mount、生命週期跟頁面一樣</td>
          <td>委派多一層、收益不明顯</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
      </tr>
      <tr>
          <td>一個元件實例、永不變動</td>
          <td>完全沒必要</td>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
      </tr>
      <tr>
          <td>需要在元件 mount 時就跑邏輯（不只回應事件）</td>
          <td>closest 只在事件發生時跑、無法當 init hook</td>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a> + MutationObserver</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="closest-失敗的處理">Closest 失敗的處理</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 點擊不在任何 shell 內
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="c1">// ...
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p><code>closest</code> 找不到時回 <code>null</code>、提早 return 是必要防護。<strong>沒這個 check 會在頁面其他地方點擊時報錯</strong>。</p>
<h3 id="從-closest-結果再往下-query">從 closest 結果再往下 query</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">var</span> <span class="nx">input</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__search-input&#39;</span><span class="p">);</span></span></span></code></pre></div><p><code>closest</code> 找到 shell 後、可以從 shell 往下 query 同元件內的其他元素 — 這是「事件 + closest + 局部 query」的組合。</p>
<h3 id="事件類型的選擇">事件類型的選擇</h3>
<table>
  <thead>
      <tr>
          <th>事件</th>
          <th>適合</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>click</code></td>
          <td>點擊互動</td>
      </tr>
      <tr>
          <td><code>input</code></td>
          <td>輸入框文字變動（需要 bubble）</td>
      </tr>
      <tr>
          <td><code>change</code></td>
          <td>選項變動（select / radio / checkbox）</td>
      </tr>
      <tr>
          <td><code>keydown</code></td>
          <td>鍵盤快捷鍵</td>
      </tr>
      <tr>
          <td><code>focus</code> / <code>blur</code></td>
          <td>焦點移動（不 bubble、要用 <code>focusin</code> / <code>focusout</code>）</td>
      </tr>
  </tbody>
</table>
<p>注意 <code>focus</code> / <code>blur</code> 不會 bubble — 事件委派要用 <code>focusin</code> / <code>focusout</code>。</p>
<h3 id="委派的根節點選擇">委派的根節點選擇</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 選項 1：document（最寬）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">handler</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 選項 2：特定容器（縮範圍）
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">pageContainer</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;main&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">pageContainer</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">handler</span><span class="p">);</span></span></span></code></pre></div><p>縮範圍的好處是「跟其他頁面區域的 listener 不互相干擾」。預設用 document、有干擾風險才縮。</p>
<hr>
<h2 id="跟其他起點做法的關係">跟其他起點做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-document-query/">document query</a></td>
          <td>靜態、簡潔、無多實例支援</td>
      </tr>
      <tr>
          <td><a href="../pattern-component-root/">元件根變數</a></td>
          <td>靜態、shell 唯一假設</td>
      </tr>
      <tr>
          <td><a href="../pattern-root-as-parameter/">起點當參數</a></td>
          <td>靜態多實例、forEach 一次設定</td>
      </tr>
      <tr>
          <td>本卡片：closest 反向找根</td>
          <td>動態、事件驅動、無 init 時機綁定</td>
      </tr>
  </tbody>
</table>
<p>複雜度遞增、能處理的動態程度也遞增。最動態的場景才用本 pattern。</p>
<hr>
<h2 id="應用範例跨多-shell-的-scope-filter">應用範例：跨多 shell 的 scope filter</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupGlobalScopeFilter</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">scope</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-scope&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">scope</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>  <span class="c1">// 不是 scope 控制的 change
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">applyScope</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">scope</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nx">setupGlobalScopeFilter</span><span class="p">();</span></span></span></code></pre></div><p>一個 listener 處理所有 shell 的 scope 變動 — 不論 shell 是初始 mount 的、還是 runtime 注入的。</p>
<hr>
<h2 id="應用範例與-起點當參數-組合">應用範例：與 <a href="../pattern-root-as-parameter/">起點當參數</a> 組合</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 初始化階段：對已存在的 shell 做 setup
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">setupSearchShell</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 事件階段：用 closest 處理可能新加的 shell
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="kd">var</span> <span class="nx">shell</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">shell</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="c1">// 處理事件、不論 shell 是初始的還是後加的
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="p">});</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">// MutationObserver：捕捉新加的 shell 做 setup
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"></span><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">mutations</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">mutations</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">m</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">m</span><span class="p">.</span><span class="nx">addedNodes</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">node</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">node</span><span class="p">.</span><span class="nx">matches</span> <span class="o">&amp;&amp;</span> <span class="nx">node</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="s1">&#39;.search-shell&#39;</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="nx">setupSearchShell</span><span class="p">(</span><span class="nx">node</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}).</span><span class="nx">observe</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><p>三個 pattern 組合：「靜態 setup」+「事件動態」+「mount 時 setup」 — 各 pattern 補不同時間點的需求。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>元件 SPA 路由動態切換</td>
          <td>是 — 直接對應使用情境</td>
      </tr>
      <tr>
          <td>元件數量大、每實例都要綁 listener</td>
          <td>是 — 委派省記憶體</td>
      </tr>
      <tr>
          <td>AJAX / Web Component runtime 注入</td>
          <td>是 — 不需要重綁</td>
      </tr>
      <tr>
          <td>確定元件靜態、生命週期固定</td>
          <td>否 — <a href="../pattern-root-as-parameter/">起點當參數</a> 已夠</td>
      </tr>
      <tr>
          <td>邏輯不是事件驅動（init 時就要跑）</td>
          <td>否 — closest 只在事件發生時跑</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：closest 反向找根把「定位元件」從綁定時延後到事件發生時 — 換到動態能力、付出的是事件處理多一層判斷。靜態場景用更簡單的做法、動態場景才升級到本 pattern。</p>
]]></content:encoded></item><item><title>Pattern：DOM attribute idempotency 標記</title><link>https://tarrragon.github.io/blog/report/pattern-attribute-idempotency-marker/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-attribute-idempotency-marker/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result:not([data-scoped])&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>apply 函式入口用 &lt;code>:not([data-x])&lt;/code> 過濾掉已處理元素、處理完後設 attribute 標記。下次 apply 被觸發時、已處理的元素不會被命中。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>把「保證只處理一次」的責任從&lt;strong>呼叫端&lt;/strong>（要記得只呼叫一次）轉到&lt;strong>元素本身&lt;/strong>（看自己有沒有被處理過）。&lt;/p>
&lt;p>apply 函式可能被多個源觸發：&lt;/p>
&lt;ul>
&lt;li>初始化時呼叫&lt;/li>
&lt;li>MutationObserver 偵測到變動觸發&lt;/li>
&lt;li>使用者事件觸發&lt;/li>
&lt;li>Framework 重繪後重新呼叫&lt;/li>
&lt;/ul>
&lt;p>任一個源多呼叫就重複處理 — 無法靠呼叫端紀律避免。Idempotency 標記讓 apply 自己防護。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Production apply 函式、可能被多源觸發&lt;/td>
 &lt;td>標記在元素上、不依賴呼叫紀律&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>處理動作有副作用（綁 listener、改 class）&lt;/td>
 &lt;td>重複觸發會疊加副作用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元素生命週期跟 attribute 同步（不會被 reset）&lt;/td>
 &lt;td>標記跟著元素走、自然清理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Devtools debug 友善&lt;/td>
 &lt;td>attribute 在 inspector 可見&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：元素的 attribute 跟著元素 DOM 生命週期、元素移除時標記自動消失。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼不夠&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫第三方 library&lt;/td>
 &lt;td>在使用者 DOM 加自家 attribute、有命名衝突風險&lt;/td>
 &lt;td>&lt;a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 重繪會清掉 attribute&lt;/td>
 &lt;td>標記消失、防護失效&lt;/td>
 &lt;td>配合 disconnect/observe 或改 WeakMap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要週期性 reset 標記&lt;/td>
 &lt;td>attribute 改回需要遍歷所有元素&lt;/td>
 &lt;td>WeakMap 可整批 &lt;code>new WeakMap()&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多種獨立的 idempotency 維度&lt;/td>
 &lt;td>DOM 上多 attribute 互相干擾&lt;/td>
 &lt;td>WeakMap 各別管理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="attribute-命名規範">Attribute 命名規範&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 好：明確 namespace + 用途
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-search-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-myapp-processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// 較差：通用名、容易跟其他程式撞
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;processed&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// 不是 data-* 開頭、可能不被 HTML spec 接受
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設用 &lt;code>data-{appname}-{purpose}&lt;/code> 格式 — 即使引入第三方 library 加 attribute、也不會撞名。&lt;/p>
&lt;h3 id="attribute-值的選擇">Attribute 值的選擇&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 1：固定 &amp;#39;true&amp;#39;（最簡）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;true&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 2：紀錄處理時間 / 版本（debug 友善）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">String&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;v2&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 3：boolean attribute（無值）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">setAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;data-scoped&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s1">&amp;#39;&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1">// CSS 用 [data-scoped] 即可選中
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設用 &lt;code>'true'&lt;/code>、debug 困難時改 timestamp 看處理順序。&lt;/p>
&lt;h3 id="跟-framework-重繪共處">跟 framework 重繪共處&lt;/h3>
&lt;p>Svelte / React / Vue 重繪元素時、&lt;strong>自家 attribute 通常會被保留&lt;/strong>（framework 只管自己的 attribute）— 但有例外：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>行為&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Framework re-render 整段 DOM&lt;/td>
 &lt;td>元素被替換、新元素沒標記 → apply 重跑、合理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework patch 既有元素 attribute&lt;/td>
 &lt;td>自家 attribute 保留&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework &lt;code>replaceWith&lt;/code> / &lt;code>innerHTML&lt;/code> 重設&lt;/td>
 &lt;td>元素被替換 → 標記消失、apply 重跑、合理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心觀察&lt;/strong>：自家 attribute 跟著元素走 — 元素還在就有、元素被換就沒。這是「正確」行為、不是 bug。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result:not([data-scoped])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>apply 函式入口用 <code>:not([data-x])</code> 過濾掉已處理元素、處理完後設 attribute 標記。下次 apply 被觸發時、已處理的元素不會被命中。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>把「保證只處理一次」的責任從<strong>呼叫端</strong>（要記得只呼叫一次）轉到<strong>元素本身</strong>（看自己有沒有被處理過）。</p>
<p>apply 函式可能被多個源觸發：</p>
<ul>
<li>初始化時呼叫</li>
<li>MutationObserver 偵測到變動觸發</li>
<li>使用者事件觸發</li>
<li>Framework 重繪後重新呼叫</li>
</ul>
<p>任一個源多呼叫就重複處理 — 無法靠呼叫端紀律避免。Idempotency 標記讓 apply 自己防護。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Production apply 函式、可能被多源觸發</td>
          <td>標記在元素上、不依賴呼叫紀律</td>
      </tr>
      <tr>
          <td>處理動作有副作用（綁 listener、改 class）</td>
          <td>重複觸發會疊加副作用</td>
      </tr>
      <tr>
          <td>元素生命週期跟 attribute 同步（不會被 reset）</td>
          <td>標記跟著元素走、自然清理</td>
      </tr>
      <tr>
          <td>Devtools debug 友善</td>
          <td>attribute 在 inspector 可見</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：元素的 attribute 跟著元素 DOM 生命週期、元素移除時標記自動消失。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library</td>
          <td>在使用者 DOM 加自家 attribute、有命名衝突風險</td>
          <td><a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a></td>
      </tr>
      <tr>
          <td>Framework 重繪會清掉 attribute</td>
          <td>標記消失、防護失效</td>
          <td>配合 disconnect/observe 或改 WeakMap</td>
      </tr>
      <tr>
          <td>需要週期性 reset 標記</td>
          <td>attribute 改回需要遍歷所有元素</td>
          <td>WeakMap 可整批 <code>new WeakMap()</code></td>
      </tr>
      <tr>
          <td>多種獨立的 idempotency 維度</td>
          <td>DOM 上多 attribute 互相干擾</td>
          <td>WeakMap 各別管理</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="attribute-命名規範">Attribute 命名規範</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 好：明確 namespace + 用途
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-myapp-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// 較差：通用名、容易跟其他程式撞
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>  <span class="c1">// 不是 data-* 開頭、可能不被 HTML spec 接受
</span></span></span></code></pre></div><p>預設用 <code>data-{appname}-{purpose}</code> 格式 — 即使引入第三方 library 加 attribute、也不會撞名。</p>
<h3 id="attribute-值的選擇">Attribute 值的選擇</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 用法 1：固定 &#39;true&#39;（最簡）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 用法 2：紀錄處理時間 / 版本（debug 友善）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="nb">String</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()));</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;v2&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 用法 3：boolean attribute（無值）
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">// CSS 用 [data-scoped] 即可選中
</span></span></span></code></pre></div><p>預設用 <code>'true'</code>、debug 困難時改 timestamp 看處理順序。</p>
<h3 id="跟-framework-重繪共處">跟 framework 重繪共處</h3>
<p>Svelte / React / Vue 重繪元素時、<strong>自家 attribute 通常會被保留</strong>（framework 只管自己的 attribute）— 但有例外：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Framework re-render 整段 DOM</td>
          <td>元素被替換、新元素沒標記 → apply 重跑、合理</td>
      </tr>
      <tr>
          <td>Framework patch 既有元素 attribute</td>
          <td>自家 attribute 保留</td>
      </tr>
      <tr>
          <td>Framework <code>replaceWith</code> / <code>innerHTML</code> 重設</td>
          <td>元素被替換 → 標記消失、apply 重跑、合理</td>
      </tr>
  </tbody>
</table>
<p><strong>核心觀察</strong>：自家 attribute 跟著元素走 — 元素還在就有、元素被換就沒。這是「正確」行為、不是 bug。</p>
<h3 id="例外framework-主動清自家-attribute">例外：framework 主動清自家 attribute</h3>
<p>少數 framework 會 strict 清非預期的 attribute（例如某些 Web Component lib）。檢查方式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// ... 等 framework patch 一次後
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">el</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">));</span>  <span class="c1">// 還在嗎？
</span></span></span></code></pre></div><p>如果消失、改用 <a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a>。</p>
<hr>
<h2 id="跟其他-idempotency-做法的關係">跟其他 idempotency 做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「過濾」維度有三種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>本卡片：DOM attribute 標記</td>
          <td>production 預設、devtools 可見、有命名衝突風險</td>
      </tr>
      <tr>
          <td><a href="../pattern-weakmap-idempotency-record/">WeakMap 紀錄</a></td>
          <td>不污染 DOM、適合 library、debug 不便</td>
      </tr>
      <tr>
          <td>依賴外部呼叫者保證</td>
          <td>反模式、無防護、不可靠</td>
      </tr>
  </tbody>
</table>
<p>預設用本卡片、第三方 library / framework 衝突情境升級到 WeakMap。</p>
<hr>
<h2 id="應用範例完整-apply">應用範例：完整 apply</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">newResults</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="s1">&#39;.pagefind-ui__result:not([data-search-scoped])&#39;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nx">newResults</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">bindClickHandler</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">addCustomBadge</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">// 多源觸發都安全
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="nx">init</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="nx">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nx">observer</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="p">...);</span>  <span class="c1">// 觀察到變動觸發 apply
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="nx">apply</span><span class="p">(</span><span class="nx">shell</span><span class="p">);</span>  <span class="c1">// 初始化時跑一次
</span></span></span></code></pre></div><p>三個觸發點任一個多跑、<code>:not([data-search-scoped])</code> 都會過濾掉已處理元素。</p>
<hr>
<h2 id="應用範例多維度標記">應用範例：多維度標記</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 三個獨立 idempotency 維度、各自 attribute
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-scoped&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>     <span class="c1">// scope filter 處理過
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-bound&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>      <span class="c1">// event listener 綁過
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span><span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-search-decorated&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>  <span class="c1">// 視覺裝飾加過
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">// 各 apply 函式只看自己的 attribute
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">applyScope</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x:not([data-search-scoped])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(...)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">function</span> <span class="nx">applyBindings</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x:not([data-search-bound])&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(...)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>每個 idempotency 維度獨立 — 互相不干擾。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Apply 被多源觸發、產生重複處理 bug</td>
          <td>是 — 直接對應使用情境</td>
      </tr>
      <tr>
          <td>寫第三方 library / 不能污染 DOM</td>
          <td>否 — 改 <a href="../pattern-weakmap-idempotency-record/">WeakMap</a></td>
      </tr>
      <tr>
          <td>Framework 會清自家 attribute</td>
          <td>否 — 改 WeakMap</td>
      </tr>
      <tr>
          <td>想在 devtools inspector 直接看處理狀態</td>
          <td>是 — attribute 可見性是優點</td>
      </tr>
      <tr>
          <td>同元素多種 idempotency 維度</td>
          <td>是 — 多 attribute 各自管理</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：把 idempotency 責任從呼叫端搬到元素本身、attribute 是「便宜可見的旗標」。Production apply 預設用本 pattern、特殊情境（library / framework 衝突）才升級到 WeakMap。</p>
]]></content:encoded></item><item><title>Pattern：WeakMap idempotency 紀錄</title><link>https://tarrragon.github.io/blog/report/pattern-weakmap-idempotency-record/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-weakmap-idempotency-record/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">processed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">WeakMap&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">shell&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelectorAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.pagefind-ui__result&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">forEach&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">has&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... 處理
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>把「已處理」狀態紀錄在 JS 的 WeakMap 裡、不寫到 DOM 上。WeakMap key 是元素本身、元素被 GC 時自動清理。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>兩件事 &lt;a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記&lt;/a> 做不到：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>不污染 DOM&lt;/strong>：使用者 DOM 不會被加自家 attribute、適合第三方 library&lt;/li>
&lt;li>&lt;strong>跟 framework 完全解耦&lt;/strong>：framework 怎麼操作 DOM 都不影響 WeakMap 紀錄&lt;/li>
&lt;/ol>
&lt;p>代價是 debug 不便（看不到狀態）、紀錄跟 JS context 綁定（換頁就消失）。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫第三方 library / npm package&lt;/td>
 &lt;td>不在使用者 DOM 加 attribute、避免命名衝突&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 會清非預期的 attribute&lt;/td>
 &lt;td>WeakMap 不在 DOM、framework 動不到&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要週期性 reset 紀錄&lt;/td>
 &lt;td>&lt;code>processed = new WeakMap()&lt;/code> 一行重置全部&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>紀錄複雜資料、不只是 boolean&lt;/td>
 &lt;td>WeakMap value 可以是任何物件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：紀錄獨立於 DOM 之外、跟 JS 物件 lifetime 綁定。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼不夠&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>自家 application、devtools debug 重要&lt;/td>
 &lt;td>看不到狀態、debug 困難&lt;/td>
 &lt;td>&lt;a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨頁面 / 跨 session 的 idempotency&lt;/td>
 &lt;td>WeakMap 在 JS context 內、換頁就消失&lt;/td>
 &lt;td>LocalStorage / 後端紀錄&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>元素生命週期短、頻繁 GC&lt;/td>
 &lt;td>WeakMap 自動清理可能比預期早&lt;/td>
 &lt;td>改用 Map（但要手動清理）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>紀錄要跟 SSR 同步&lt;/td>
 &lt;td>WeakMap 只活在 client&lt;/td>
 &lt;td>結合 attribute（SSR 階段標記）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="為什麼用-weakmap-不用-map--set">為什麼用 WeakMap 不用 Map / Set&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// WeakMap：key 是元素、元素被 GC 時 entry 自動消失
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">processedW&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">WeakMap&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">processedW&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1">// el 從 DOM 移除 + 沒其他 reference → GC → WeakMap entry 消失
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1">// Map / Set：強引用、阻止 GC
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kd">var&lt;/span> &lt;span class="nx">processedS&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">Set&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="nx">processedS&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1">// el 從 DOM 移除、但 Set 還抓著 → 永久 leak
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM 元素可能動態移除（filter、SPA 路由切換、framework 重繪）— Map / Set 會造成 memory leak。&lt;strong>處理 DOM 元素 idempotency 預設用 WeakMap&lt;/strong>。&lt;/p>
&lt;h3 id="value-的設計">Value 的設計&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 1：純 boolean（最簡）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 2：紀錄處理版本（升級時偵測 stale 紀錄）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">version&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">time&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">has&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">version&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="nx">currentVersion&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">// 用法 3：紀錄相關 metadata（避免重複查詢）
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="nx">processed&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">bindingsId&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">registerListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">initialClass&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>WeakMap value 可以儲任何資料 — 比 attribute（只能存字串）更彈性。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>  <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>把「已處理」狀態紀錄在 JS 的 WeakMap 裡、不寫到 DOM 上。WeakMap key 是元素本身、元素被 GC 時自動清理。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>兩件事 <a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a> 做不到：</p>
<ol>
<li><strong>不污染 DOM</strong>：使用者 DOM 不會被加自家 attribute、適合第三方 library</li>
<li><strong>跟 framework 完全解耦</strong>：framework 怎麼操作 DOM 都不影響 WeakMap 紀錄</li>
</ol>
<p>代價是 debug 不便（看不到狀態）、紀錄跟 JS context 綁定（換頁就消失）。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library / npm package</td>
          <td>不在使用者 DOM 加 attribute、避免命名衝突</td>
      </tr>
      <tr>
          <td>Framework 會清非預期的 attribute</td>
          <td>WeakMap 不在 DOM、framework 動不到</td>
      </tr>
      <tr>
          <td>需要週期性 reset 紀錄</td>
          <td><code>processed = new WeakMap()</code> 一行重置全部</td>
      </tr>
      <tr>
          <td>紀錄複雜資料、不只是 boolean</td>
          <td>WeakMap value 可以是任何物件</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：紀錄獨立於 DOM 之外、跟 JS 物件 lifetime 綁定。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自家 application、devtools debug 重要</td>
          <td>看不到狀態、debug 困難</td>
          <td><a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a></td>
      </tr>
      <tr>
          <td>跨頁面 / 跨 session 的 idempotency</td>
          <td>WeakMap 在 JS context 內、換頁就消失</td>
          <td>LocalStorage / 後端紀錄</td>
      </tr>
      <tr>
          <td>元素生命週期短、頻繁 GC</td>
          <td>WeakMap 自動清理可能比預期早</td>
          <td>改用 Map（但要手動清理）</td>
      </tr>
      <tr>
          <td>紀錄要跟 SSR 同步</td>
          <td>WeakMap 只活在 client</td>
          <td>結合 attribute（SSR 階段標記）</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="為什麼用-weakmap-不用-map--set">為什麼用 WeakMap 不用 Map / Set</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// WeakMap：key 是元素、元素被 GC 時 entry 自動消失
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">processedW</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">processedW</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// el 從 DOM 移除 + 沒其他 reference → GC → WeakMap entry 消失
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">// Map / Set：強引用、阻止 GC
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">processedS</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Set</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="nx">processedS</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">// el 從 DOM 移除、但 Set 還抓著 → 永久 leak
</span></span></span></code></pre></div><p>DOM 元素可能動態移除（filter、SPA 路由切換、framework 重繪）— Map / Set 會造成 memory leak。<strong>處理 DOM 元素 idempotency 預設用 WeakMap</strong>。</p>
<h3 id="value-的設計">Value 的設計</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 用法 1：純 boolean（最簡）
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">// 用法 2：紀錄處理版本（升級時偵測 stale 紀錄）
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span> <span class="nx">version</span><span class="o">:</span> <span class="mi">2</span><span class="p">,</span> <span class="nx">time</span><span class="o">:</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">processed</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">el</span><span class="p">).</span><span class="nx">version</span> <span class="o">===</span> <span class="nx">currentVersion</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">// 用法 3：紀錄相關 metadata（避免重複查詢）
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"></span><span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nx">bindingsId</span><span class="o">:</span> <span class="nx">registerListener</span><span class="p">(</span><span class="nx">el</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="nx">initialClass</span><span class="o">:</span> <span class="nx">el</span><span class="p">.</span><span class="nx">className</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>WeakMap value 可以儲任何資料 — 比 attribute（只能存字串）更彈性。</p>
<h3 id="debug-替代方案">Debug 替代方案</h3>
<p>attribute 標記可以在 devtools inspector 直接看；WeakMap 看不到。debug 時的替代：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 開發模式同步寫一份 attribute（production build 時拿掉）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">markProcessed</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">DEV_MODE</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-debug-processed&#39;</span><span class="p">,</span> <span class="s1">&#39;true&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>或暴露到 console：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">window</span><span class="p">.</span><span class="nx">__debug_processed</span> <span class="o">=</span> <span class="nx">processed</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// console: __debug_processed.has($0)  // 檢查當前選中元素
</span></span></span></code></pre></div><p>這些都是 workaround、不如 attribute 標記直觀。<strong>選 WeakMap 的人通常已經接受這個 debug 成本</strong>。</p>
<h3 id="reset-紀錄">Reset 紀錄</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// WeakMap 整批 reset
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">// 對比 attribute 整批 reset 要遍歷
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;[data-scoped]&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">el</span><span class="p">.</span><span class="nx">removeAttribute</span><span class="p">(</span><span class="s1">&#39;data-scoped&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>需要週期性 reset（例如 user 切換 mode、所有元素該重新處理）— WeakMap 一行解決、attribute 要遍歷。</p>
<hr>
<h2 id="跟其他-idempotency-做法的關係">跟其他 idempotency 做法的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「過濾」維度有三種做法：</p>
<table>
  <thead>
      <tr>
          <th>做法</th>
          <th>比較</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="../pattern-attribute-idempotency-marker/">DOM attribute 標記</a></td>
          <td>production 預設、devtools 可見、有命名衝突風險</td>
      </tr>
      <tr>
          <td>本卡片：WeakMap 紀錄</td>
          <td>不污染 DOM、適合 library、debug 不便</td>
      </tr>
      <tr>
          <td>依賴外部呼叫者保證</td>
          <td>反模式、無防護</td>
      </tr>
  </tbody>
</table>
<p>選擇順序：<strong>自家 application</strong> → attribute；<strong>library / framework 衝突</strong> → WeakMap；<strong>反模式不選</strong>。</p>
<hr>
<h2 id="應用範例library-設計">應用範例：library 設計</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 第三方 library export 的 init 函式
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kd">function</span> <span class="nx">initSearchEnhancement</span><span class="p">(</span><span class="nx">shell</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.search-result&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">processed</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      <span class="nx">enhanceResult</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">apply</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="nx">apply</span><span class="p">).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nx">subtree</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1">// 使用者：
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"></span><span class="nx">initSearchEnhancement</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.my-search&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1">// 不會在使用者 DOM 上加任何 data-* attribute
</span></span></span></code></pre></div><p>使用者 DOM 完全乾淨、library 行為內聚。</p>
<hr>
<h2 id="應用範例版本化處理">應用範例：版本化處理</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">processed</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">var</span> <span class="nx">CURRENT_VERSION</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.x&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kd">var</span> <span class="nx">record</span> <span class="o">=</span> <span class="nx">processed</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">el</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">record</span> <span class="o">&amp;&amp;</span> <span class="nx">record</span><span class="p">.</span><span class="nx">version</span> <span class="o">===</span> <span class="nx">CURRENT_VERSION</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1">// 升級到新版本（可能需要清舊綁定）
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>    <span class="k">if</span> <span class="p">(</span><span class="nx">record</span><span class="p">)</span> <span class="nx">cleanup</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="nx">record</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">enhance</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="nx">CURRENT_VERSION</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">processed</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="p">{</span> <span class="nx">version</span><span class="o">:</span> <span class="nx">CURRENT_VERSION</span><span class="p">,</span> <span class="nx">time</span><span class="o">:</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>版本變動時 — 不需要遍歷 DOM 清舊 attribute、直接用 WeakMap value 比對。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫第三方 library / npm package</td>
          <td>是 — 不污染使用者 DOM</td>
      </tr>
      <tr>
          <td>Framework 會 strict 清自家 attribute</td>
          <td>是 — WeakMap 跟 framework 解耦</td>
      </tr>
      <tr>
          <td>紀錄需要儲複雜資料（不只 boolean）</td>
          <td>是 — WeakMap value 可任意</td>
      </tr>
      <tr>
          <td>自家 application、debug 重要</td>
          <td>否 — <a href="../pattern-attribute-idempotency-marker/">attribute 標記</a> 在 inspector 可見</td>
      </tr>
      <tr>
          <td>紀錄要跨頁面持久化</td>
          <td>否 — 改用 storage / 後端</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：WeakMap idempotency 是 attribute 標記的「不污染 DOM 替代品」 — 在 library / framework 衝突情境必要、在自家 application 通常用 attribute 即可。GC 自動清理是 WeakMap 的特性、預設不用 Map / Set 是因為它們會 memory leak。</p>
]]></content:encoded></item><item><title>Pattern：跨 slot 同節點搬遷</title><link>https://tarrragon.github.io/blog/report/pattern-cross-slot-node-relocation/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/pattern-cross-slot-node-relocation/</guid><description>&lt;h2 id="核心做法">核心做法&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: 1400px)&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matches&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="nx">desktopSlot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">insertBefore&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">drawer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">firstChild&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nx">place&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 初始化
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。&lt;/p>
&lt;hr>
&lt;h2 id="這個做法存在的價值">這個做法存在的價值&lt;/h2>
&lt;p>Stateful UI（內含 checkbox 勾選、表單值、scroll 位置等 state）跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。&lt;/p>
&lt;p>搬同一份節點 = state 永遠跟著節點走 = 切換無感。&lt;/p>
&lt;hr>
&lt;h2 id="適合的情境">適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼合理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Filter UI 跨 viewport 切換顯示位置&lt;/td>
 &lt;td>checkbox state 跟著節點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Modal 內容 vs 側邊抽屜&lt;/td>
 &lt;td>同一份表單在兩種展示方式間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tab UI 跨 desktop / mobile 重新組織&lt;/td>
 &lt;td>各 tab 內 state 不重置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>任何「同 UI、不同位置」的 responsive 切換&lt;/td>
 &lt;td>不需要 state 同步邏輯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心特徵&lt;/strong>：UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。&lt;/p>
&lt;hr>
&lt;h2 id="不適合的情境">不適合的情境&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>為什麼不夠&lt;/th>
 &lt;th>改用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>兩個位置展示的是不同元件（雖然視覺類似）&lt;/td>
 &lt;td>搬遷會把錯誤元件搬到錯位置&lt;/td>
 &lt;td>各自獨立掛載、不搬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>UI 純 stateless（純圖示、純文字）&lt;/td>
 &lt;td>複製兩份成本低、無 state 風險&lt;/td>
 &lt;td>CSS-only 雙顯示 + display 切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Framework 管的節點&lt;/td>
 &lt;td>整節點搬安全、但複製不安全（id duplicate / framework 困惑）&lt;/td>
 &lt;td>必須搬整節點、不複製&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩個位置視覺差異大&lt;/td>
 &lt;td>搬遷後 UI 不適配新位置&lt;/td>
 &lt;td>各自獨立元件&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="設計細節">設計細節&lt;/h2>
&lt;h3 id="appendchild-是搬遷不是複製">&lt;code>appendChild&lt;/code> 是搬遷、不是複製&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">parentA&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">appendChild&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">node&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="c1">// node 從原位置消失、出現在 parentA
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>DOM API 的 &lt;code>appendChild&lt;/code> / &lt;code>insertBefore&lt;/code> 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。&lt;/p>
&lt;h3 id="初始放在哪">初始放在哪&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 預設位置（mobile / fallback）--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;pagefind-ui&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;drawer&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;filter-panel&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>...&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span> &lt;span class="c">&amp;lt;!-- 初始在這 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- 桌面 slot（空、等待搬入）--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">aside&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;desktop-filter-slot&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">aside&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>預設放在 fallback 位置 — 當 JS 失敗時仍可見。&lt;/p>
&lt;h3 id="跨-slot-切換的時機">跨 slot 切換的時機&lt;/h3>
&lt;p>&lt;code>matchMedia&lt;/code> event 是 viewport 跨過 breakpoint 的瞬間：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">mql&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">window&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">matchMedia&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;(min-width: 1400px)&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nx">mql&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addEventListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;change&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">place&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nx">place&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="c1">// 初始也跑一次
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。&lt;code>matchMedia&lt;/code> 只在 cross 的瞬間觸發。&lt;/p>
&lt;h3 id="搬遷時-framework-的-reactivity">搬遷時 framework 的 reactivity&lt;/h3>
&lt;p>如果搬遷的節點是 framework 管的（如 Pagefind 的 svelte 元件）— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心做法">核心做法</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: 1400px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">place</span><span class="p">();</span>  <span class="c1">// 初始化
</span></span></span></code></pre></div><p>同一個 DOM 節點在兩個 slot 之間搬移、不複製成兩份。</p>
<hr>
<h2 id="這個做法存在的價值">這個做法存在的價值</h2>
<p>Stateful UI（內含 checkbox 勾選、表單值、scroll 位置等 state）跨兩個顯示位置切換時、複製兩份會造成 state 分歧 — 使用者在 desktop 勾的 filter、切到 mobile 看不到勾選狀態。</p>
<p>搬同一份節點 = state 永遠跟著節點走 = 切換無感。</p>
<hr>
<h2 id="適合的情境">適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼合理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filter UI 跨 viewport 切換顯示位置</td>
          <td>checkbox state 跟著節點</td>
      </tr>
      <tr>
          <td>Modal 內容 vs 側邊抽屜</td>
          <td>同一份表單在兩種展示方式間</td>
      </tr>
      <tr>
          <td>Tab UI 跨 desktop / mobile 重新組織</td>
          <td>各 tab 內 state 不重置</td>
      </tr>
      <tr>
          <td>任何「同 UI、不同位置」的 responsive 切換</td>
          <td>不需要 state 同步邏輯</td>
      </tr>
  </tbody>
</table>
<p><strong>核心特徵</strong>：UI 內含 state、兩個位置展示的是「同一個邏輯單位」、不是「兩個獨立元件」。</p>
<hr>
<h2 id="不適合的情境">不適合的情境</h2>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼不夠</th>
          <th>改用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>兩個位置展示的是不同元件（雖然視覺類似）</td>
          <td>搬遷會把錯誤元件搬到錯位置</td>
          <td>各自獨立掛載、不搬</td>
      </tr>
      <tr>
          <td>UI 純 stateless（純圖示、純文字）</td>
          <td>複製兩份成本低、無 state 風險</td>
          <td>CSS-only 雙顯示 + display 切換</td>
      </tr>
      <tr>
          <td>Framework 管的節點</td>
          <td>整節點搬安全、但複製不安全（id duplicate / framework 困惑）</td>
          <td>必須搬整節點、不複製</td>
      </tr>
      <tr>
          <td>兩個位置視覺差異大</td>
          <td>搬遷後 UI 不適配新位置</td>
          <td>各自獨立元件</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="設計細節">設計細節</h2>
<h3 id="appendchild-是搬遷不是複製"><code>appendChild</code> 是搬遷、不是複製</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">parentA</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">node</span><span class="p">);</span>  <span class="c1">// node 從原位置消失、出現在 parentA
</span></span></span></code></pre></div><p>DOM API 的 <code>appendChild</code> / <code>insertBefore</code> 是 move、不是 copy — 同一個節點不能同時存在於多個位置。這個特性正是搬遷 pattern 的基礎。</p>
<h3 id="初始放在哪">初始放在哪</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">&lt;!-- 預設位置（mobile / fallback）--&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;pagefind-ui&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;drawer&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;filter-panel&#34;</span><span class="p">&gt;</span>...<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>  <span class="c">&lt;!-- 初始在這 --&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c">&lt;!-- 桌面 slot（空、等待搬入）--&gt;</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">&lt;</span><span class="nt">aside</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;desktop-filter-slot&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">aside</span><span class="p">&gt;</span></span></span></code></pre></div><p>預設放在 fallback 位置 — 當 JS 失敗時仍可見。</p>
<h3 id="跨-slot-切換的時機">跨 slot 切換的時機</h3>
<p><code>matchMedia</code> event 是 viewport 跨過 breakpoint 的瞬間：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: 1400px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">place</span><span class="p">();</span>  <span class="c1">// 初始也跑一次
</span></span></span></code></pre></div><p>不要用 resize event — 太頻繁、會在 breakpoint 邊界震盪。<code>matchMedia</code> 只在 cross 的瞬間觸發。</p>
<h3 id="搬遷時-framework-的-reactivity">搬遷時 framework 的 reactivity</h3>
<p>如果搬遷的節點是 framework 管的（如 Pagefind 的 svelte 元件）— 整節點搬通常安全、framework 在下次 patch 時看到節點還在、繼續更新內部。</p>
<p>詳細安全規則由 <a href="../component-boundary-and-js-impact/">#13 JS 操作 framework 元件：邊界辨識與安全規則</a> 處理。</p>
<h3 id="focus-跟著搬">Focus 跟著搬</h3>
<p>搬遷可能讓鍵盤 focus 暫時失去（視瀏覽器）— 加 save/restore：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">else</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>詳細處理由 <a href="../focus-management-on-dom-move/">#37 動態 DOM 移動時的 focus 管理</a> 處理。</p>
<hr>
<h2 id="設計取捨兩個-slot-的-stateful-ui-共用">設計取捨：兩個 slot 的 stateful UI 共用</h2>
<p>四種做法、各自機會成本不同。預設選 A（搬同節點）、其他做法在特定情境合理。</p>
<h3 id="a搬同一節點這個專案的預設">A：搬同一節點（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：<code>matchMedia + appendChild</code> 在兩 slot 間搬同一份節點</li>
<li><strong>選 A 的理由</strong>：state 跟著節點、切換無感、不需要 sync 邏輯</li>
<li><strong>適合</strong>：stateful UI、需要在兩個位置展示同樣內容</li>
<li><strong>代價</strong>：搬遷 callback 在 viewport 跨 breakpoint 時觸發、需要處理 focus / 動畫</li>
<li><strong>詳細</strong>：本卡片</li>
</ul>
<h3 id="bcss-only-雙顯示--display-切換">B：CSS-only 雙顯示 + display 切換</h3>
<ul>
<li><strong>機制</strong>：兩個位置都放同一份節點 (寫兩遍 HTML)、用 <code>@media + display: none</code> 切換顯示</li>
<li><strong>跟 A 的取捨</strong>：B 純 CSS 簡單、A 需要 JS；但 B 對 stateful UI 失敗（兩份 state 各自獨立）</li>
<li><strong>B 比 A 好的情境</strong>：UI 純 stateless（純圖示）、純 CSS 解就夠</li>
</ul>
<h3 id="ccss-only--js-同步-state">C：CSS-only + JS 同步 state</h3>
<ul>
<li><strong>機制</strong>：兩份節點 + JS 監聽 state 變動同步</li>
<li><strong>跟 A 的取捨</strong>：C 比 B 解 state 問題、但同步邏輯複雜（雙向更新、避免循環）</li>
<li><strong>C 比 A 好的情境</strong>：兩個位置的 UI 視覺需要差異（不只是位置不同）</li>
</ul>
<h3 id="djs-完全重建-ui">D：JS 完全重建 UI</h3>
<ul>
<li><strong>機制</strong>：viewport 變動時拆掉舊 UI、在新位置重建一份</li>
<li><strong>成本特別高的原因</strong>：state 在重建時遺失、UI 閃爍、輸入中斷</li>
<li><strong>D 才合理的情境</strong>：UI 是 stateless 的、且重建成本低</li>
</ul>
<hr>
<h2 id="跟其他-pattern-的關係">跟其他 pattern 的關係</h2>
<p><a href="../dom-selector-precision/">#14 Selector 精準度</a> 的「起點」維度有四種做法、本卡片是「跨 slot 搬遷」這個專門情境的補充：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>對應 pattern</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query 的起點</td>
          <td><a href="../pattern-document-query/">#46 document</a> / <a href="../pattern-component-root/">#47 元件根變數</a> / <a href="../pattern-root-as-parameter/">#48 起點當參數</a> / <a href="../pattern-closest-lookup/">#49 closest 反向</a></td>
      </tr>
      <tr>
          <td>Idempotency 過濾</td>
          <td><a href="../pattern-attribute-idempotency-marker/">#50 attribute 標記</a> / <a href="../pattern-weakmap-idempotency-record/">#51 WeakMap</a></td>
      </tr>
      <tr>
          <td>跨 slot 搬遷（本卡片）</td>
          <td>同節點 vs 雙節點 + state 同步</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="應用範例跨-viewport-filter-切換">應用範例：跨 viewport filter 切換</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">function</span> <span class="nx">setupResponsiveFilter</span><span class="p">(</span><span class="nx">shell</span><span class="p">,</span> <span class="nx">breakpoint</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">var</span> <span class="nx">filter</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__filter-panel&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kd">var</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nx">shell</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">var</span> <span class="nx">desktopSlot</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-filter-slot&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">filter</span> <span class="o">||</span> <span class="o">!</span><span class="nx">drawer</span> <span class="o">||</span> <span class="o">!</span><span class="nx">desktopSlot</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">var</span> <span class="nx">mql</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">matchMedia</span><span class="p">(</span><span class="s1">&#39;(min-width: &#39;</span> <span class="o">+</span> <span class="nx">breakpoint</span> <span class="o">+</span> <span class="s1">&#39;px)&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="kd">function</span> <span class="nx">place</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="kd">var</span> <span class="nx">activeBefore</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">activeElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">mql</span><span class="p">.</span><span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">      <span class="nx">desktopSlot</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">filter</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">      <span class="nx">drawer</span><span class="p">.</span><span class="nx">insertBefore</span><span class="p">(</span><span class="nx">filter</span><span class="p">,</span> <span class="nx">drawer</span><span class="p">.</span><span class="nx">firstChild</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">activeBefore</span> <span class="o">&amp;&amp;</span> <span class="nx">filter</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="nx">activeBefore</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">      <span class="nx">activeBefore</span><span class="p">.</span><span class="nx">focus</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="nx">place</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">  <span class="nx">mql</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="nx">place</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>完整 pattern：取元件根 + matchMedia + 搬遷 + focus 處理。</p>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該套用本 pattern 嗎？</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>兩份節點各自 state、用 sync 邏輯保持一致</td>
          <td>是 — 改成搬同節點、移除 sync</td>
      </tr>
      <tr>
          <td>Stateful UI 在 mobile / desktop 兩種 layout 間</td>
          <td>是 — 直接的應用</td>
      </tr>
      <tr>
          <td>切換 viewport 時 UI 閃爍 / 重建</td>
          <td>是 — 改成搬而非重建</td>
      </tr>
      <tr>
          <td>兩個位置展示完全不同的 UI（不是同邏輯）</td>
          <td>否 — 各自獨立元件</td>
      </tr>
      <tr>
          <td>Framework 管的節點</td>
          <td>是 — 整節點搬安全、但要遵守 <a href="../component-boundary-and-js-impact/">#13</a> 的規則</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：Stateful UI 的兩個展示位置共用同一份節點、state 自然跟著走 — 比「兩份節點 + sync 邏輯」乾淨。複製兩份是「state 來源從一變二」的隱形多源（違反 <a href="../single-source-of-truth/">#44 SSoT</a>）。</p>
]]></content:encoded></item><item><title>DOM Topology First — 寫 CSS 前先確認 DOM 結構</title><link>https://tarrragon.github.io/blog/skills/frontend-with-playwright/dom-topology-first/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/skills/frontend-with-playwright/dom-topology-first/</guid><description>&lt;p>寫 CSS 規則之前、先讀真實 DOM tree — class name 是約定、不是結構保證。Selector 設計從最精準起步、有證據再放寬。&lt;/p>
&lt;p>適用：寫 / 改 CSS 規則、設計 JS query selector、判斷是否該改 layout 結構。
不適用：純邏輯演算法（沒有 DOM）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>自包含聲明&lt;/strong>：閱讀本文件不需要先讀其他 reference。本文件涵蓋 DOM 量測方法、selector 三維度設計、四種起點的取捨。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="何時參閱本文件">何時參閱本文件&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>該做的第一件事&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>即將寫 CSS 規則但只看過 class name、沒看過真實 DOM&lt;/td>
 &lt;td>playwright 量 ancestor chain&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Selector 命中超出預期的元素&lt;/td>
 &lt;td>把 selector 加上起點 + 範圍 + 過濾三維度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規則寫了但不生效&lt;/td>
 &lt;td>DevTools Computed → 看誰實際贏了&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Class name 含 &lt;code>__inner&lt;/code> &lt;code>__wrapper&lt;/code> 但不確定是直接子節點&lt;/td>
 &lt;td>playwright 讀 parent / child 關係&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>想用 &lt;code>document.querySelectorAll('.target')&lt;/code>&lt;/td>
 &lt;td>先評估「起點要不要從元件根」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="為什麼-dom-topology-要先確認">為什麼 DOM topology 要先確認&lt;/h2>
&lt;p>CSS 行為由「規則 + DOM tree + 樣式繼承 + 框架渲染」四個變數共同決定。&lt;strong>靜態推理只能基於假設的 DOM tree&lt;/strong> — 假設錯了、推理就錯。&lt;/p>
&lt;p>Class name 是命名約定 — &lt;code>pagefind-ui__drawer&lt;/code> 看起來像 &lt;code>.pagefind-ui&lt;/code> 的 child，但實際可能是 &lt;code>pagefind-ui__form&lt;/code> 的 child。命名告訴你「這是 drawer」、不告訴你「在哪一層」。&lt;/p>
&lt;p>跳過 DOM 確認的代價：寫了 N 條 CSS 規則、推理為什麼不生效、加 specificity / &lt;code>!important&lt;/code> / &lt;code>display: contents&lt;/code> — 全部基於錯假設。&lt;/p>
&lt;hr>
&lt;h2 id="量-dom-的最小-query">量 DOM 的最小 query&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// ancestor chain
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">el&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="kd">let&lt;/span> &lt;span class="nx">chain&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[];&lt;/span> &lt;span class="kd">let&lt;/span> &lt;span class="nx">n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">el&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">n&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nx">n&lt;/span> &lt;span class="o">!==&lt;/span> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">body&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="nx">chain&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">push&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">tagName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">.&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">className&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="nx">n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">n&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">parentElement&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">chain&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>返回值告訴你目標元素在 DOM tree 哪個位置、parent / sibling 是誰。寫 CSS 規則前 30 秒能省掉後續 30 分鐘推理。&lt;/p>
&lt;hr>
&lt;h2 id="selector-設計三維度">Selector 設計三維度&lt;/h2>
&lt;p>精準的 selector = &lt;strong>起點 + 範圍 + 過濾&lt;/strong> 三維度顯式設計、不是「能命中就好」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>問題&lt;/th>
 &lt;th>答案類型&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>起點&lt;/td>
 &lt;td>從哪個 DOM 節點開始 query&lt;/td>
 &lt;td>document / 元件根 / 函式參數 / closest()&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>範圍&lt;/td>
 &lt;td>要找直接子節點還是子孫&lt;/td>
 &lt;td>&lt;code>&amp;gt;&lt;/code> 直接子 / &lt;code>&amp;gt; ... &amp;gt; ...&lt;/code> 多層 / 空格 子孫&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>過濾&lt;/td>
 &lt;td>要排除哪些元素 / 已處理的&lt;/td>
 &lt;td>&lt;code>:not()&lt;/code> / &lt;code>[data-processed]&lt;/code> / WeakMap 檢查&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="起點四選一依情境">起點四選一（依情境）&lt;/h2>
&lt;h3 id="起點-adocument-全文件-query">起點 A：Document 全文件 query&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.target&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>用&lt;/strong>：原型期、單例（整頁只一個）、跨元件邊界元素。
&lt;strong>不用&lt;/strong>：production 客製、可能多實例、效能敏感（大頁面）。&lt;/p></description><content:encoded><![CDATA[<p>寫 CSS 規則之前、先讀真實 DOM tree — class name 是約定、不是結構保證。Selector 設計從最精準起步、有證據再放寬。</p>
<p>適用：寫 / 改 CSS 規則、設計 JS query selector、判斷是否該改 layout 結構。
不適用：純邏輯演算法（沒有 DOM）。</p>
<blockquote>
<p><strong>自包含聲明</strong>：閱讀本文件不需要先讀其他 reference。本文件涵蓋 DOM 量測方法、selector 三維度設計、四種起點的取捨。</p></blockquote>
<hr>
<h2 id="何時參閱本文件">何時參閱本文件</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該做的第一件事</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>即將寫 CSS 規則但只看過 class name、沒看過真實 DOM</td>
          <td>playwright 量 ancestor chain</td>
      </tr>
      <tr>
          <td>Selector 命中超出預期的元素</td>
          <td>把 selector 加上起點 + 範圍 + 過濾三維度</td>
      </tr>
      <tr>
          <td>規則寫了但不生效</td>
          <td>DevTools Computed → 看誰實際贏了</td>
      </tr>
      <tr>
          <td>Class name 含 <code>__inner</code> <code>__wrapper</code> 但不確定是直接子節點</td>
          <td>playwright 讀 parent / child 關係</td>
      </tr>
      <tr>
          <td>想用 <code>document.querySelectorAll('.target')</code></td>
          <td>先評估「起點要不要從元件根」</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="為什麼-dom-topology-要先確認">為什麼 DOM topology 要先確認</h2>
<p>CSS 行為由「規則 + DOM tree + 樣式繼承 + 框架渲染」四個變數共同決定。<strong>靜態推理只能基於假設的 DOM tree</strong> — 假設錯了、推理就錯。</p>
<p>Class name 是命名約定 — <code>pagefind-ui__drawer</code> 看起來像 <code>.pagefind-ui</code> 的 child，但實際可能是 <code>pagefind-ui__form</code> 的 child。命名告訴你「這是 drawer」、不告訴你「在哪一層」。</p>
<p>跳過 DOM 確認的代價：寫了 N 條 CSS 規則、推理為什麼不生效、加 specificity / <code>!important</code> / <code>display: contents</code> — 全部基於錯假設。</p>
<hr>
<h2 id="量-dom-的最小-query">量 DOM 的最小 query</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// ancestor chain
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="kr">const</span> <span class="nx">el</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="kd">let</span> <span class="nx">chain</span> <span class="o">=</span> <span class="p">[];</span> <span class="kd">let</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">el</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="k">while</span> <span class="p">(</span><span class="nx">n</span> <span class="o">&amp;&amp;</span> <span class="nx">n</span> <span class="o">!==</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">chain</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="sb">`</span><span class="si">${</span><span class="nx">n</span><span class="p">.</span><span class="nx">tagName</span><span class="si">}</span><span class="sb">.</span><span class="si">${</span><span class="nx">n</span><span class="p">.</span><span class="nx">className</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">n</span> <span class="o">=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="k">return</span> <span class="nx">chain</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>返回值告訴你目標元素在 DOM tree 哪個位置、parent / sibling 是誰。寫 CSS 規則前 30 秒能省掉後續 30 分鐘推理。</p>
<hr>
<h2 id="selector-設計三維度">Selector 設計三維度</h2>
<p>精準的 selector = <strong>起點 + 範圍 + 過濾</strong> 三維度顯式設計、不是「能命中就好」。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>問題</th>
          <th>答案類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>起點</td>
          <td>從哪個 DOM 節點開始 query</td>
          <td>document / 元件根 / 函式參數 / closest()</td>
      </tr>
      <tr>
          <td>範圍</td>
          <td>要找直接子節點還是子孫</td>
          <td><code>&gt;</code> 直接子 / <code>&gt; ... &gt; ...</code> 多層 / 空格 子孫</td>
      </tr>
      <tr>
          <td>過濾</td>
          <td>要排除哪些元素 / 已處理的</td>
          <td><code>:not()</code> / <code>[data-processed]</code> / WeakMap 檢查</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="起點四選一依情境">起點四選一（依情境）</h2>
<h3 id="起點-adocument-全文件-query">起點 A：Document 全文件 query</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span></span></span></code></pre></div><p><strong>用</strong>：原型期、單例（整頁只一個）、跨元件邊界元素。
<strong>不用</strong>：production 客製、可能多實例、效能敏感（大頁面）。</p>
<h3 id="起點-b元件根變數-query">起點 B：元件根變數 query</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">root</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">root</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span>  <span class="c1">// 從 root 起
</span></span></span></code></pre></div><p><strong>用</strong>：production 客製、客製只該影響該元件、避免命中其他頁面同名元素。
<strong>不用</strong>：跨多元件邊界的 query。</p>
<h3 id="起點-c起點當函式參數">起點 C：起點當函式參數</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">decorate</span><span class="p">(</span><span class="nx">root</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">return</span> <span class="nx">root</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>用</strong>：library / utility function、需要支援多實例、純函式設計。
<strong>不用</strong>：一次性腳本（多餘的抽象）。</p>
<h3 id="起點-dclosest-反向找根">起點 D：closest() 反向找根</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">button</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">,</span> <span class="nx">e</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kr">const</span> <span class="nx">card</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">closest</span><span class="p">(</span><span class="s1">&#39;.result-card&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">card</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;expanded&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p><strong>用</strong>：動態 / 多實例元件、event delegation、不知道事件源在哪一層。
<strong>不用</strong>：靜態起點已知（用 B 或 C 更直接）。</p>
<hr>
<h2 id="範圍-還是空格">範圍：<code>&gt;</code> 還是空格</h2>
<table>
  <thead>
      <tr>
          <th>寫法</th>
          <th>意思</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>.parent &gt; .child</code></td>
          <td>直接子節點</td>
          <td>安全、嚴格</td>
      </tr>
      <tr>
          <td><code>.parent .child</code></td>
          <td>任意深度子孫</td>
          <td>命中 nested 結構的同類元素</td>
      </tr>
      <tr>
          <td><code>.parent &gt; * &gt; .x</code></td>
          <td>確切兩層</td>
          <td>嚴格、結構變動時要更新</td>
      </tr>
      <tr>
          <td><code>.parent .x:not(.y)</code></td>
          <td>子孫中排除某類</td>
          <td>還是子孫範圍、:not 是過濾不是限制範圍</td>
      </tr>
  </tbody>
</table>
<p>預設 <code>&gt;</code>、有證據（多層 nested 結構都該 match）才放寬到空格。</p>
<hr>
<h2 id="過濾idempotency-標記">過濾：idempotency 標記</h2>
<p>JS 處理元素時、避免重複處理。兩種做法：</p>
<h3 id="adom-attribute-標記">A：DOM attribute 標記</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">decorate</span><span class="p">(</span><span class="nx">root</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kr">const</span> <span class="nx">targets</span> <span class="o">=</span> <span class="nx">root</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.target:not([data-decorated])&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">targets</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span>    <span class="nx">el</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;data-decorated&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>用</strong>：production 預設、devtools 可見、跨 page reload 也保留（如果元素持久）。
<strong>不用</strong>：library 設計（不該污染使用者 DOM）。</p>
<h3 id="bweakmap-紀錄">B：WeakMap 紀錄</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">decorated</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WeakMap</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kd">function</span> <span class="nx">decorate</span><span class="p">(</span><span class="nx">root</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">root</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.target&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">decorated</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="c1">// ... 處理
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"></span>    <span class="nx">decorated</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><strong>用</strong>：library 設計、不污染 DOM、元素 GC 後紀錄自動清。
<strong>不用</strong>：跨頁面、需要 devtools debug、需要 CSS selector 過濾（CSS 看不到 WeakMap）。</p>
<hr>
<h2 id="wrong-vs-right-對照">Wrong vs Right 對照</h2>
<h3 id="範例-1寫-css-前沒看-dom">範例 1：寫 CSS 前沒看 DOM</h3>
<blockquote>
<p>任務：把 <code>pagefind-ui__drawer</code> 排到 <code>pagefind-ui__form</code> 下方</p></blockquote>
<p><strong>錯</strong>（基於 class 命名假設）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="k">display</span><span class="p">:</span> <span class="k">grid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="k">grid-template-rows</span><span class="p">:</span> <span class="kc">auto</span> <span class="kc">auto</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__form</span> <span class="p">{</span> <span class="k">grid-row</span><span class="p">:</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">.</span><span class="nc">pagefind-ui__drawer</span> <span class="p">{</span> <span class="k">grid-row</span><span class="p">:</span> <span class="mi">2</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div><p>跑出來 drawer 跑到頁尾、grid-row 完全沒生效。</p>
<p><strong>對</strong>（先量 DOM）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kr">const</span> <span class="nx">drawer</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui__drawer&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">let</span> <span class="nx">chain</span> <span class="o">=</span> <span class="p">[];</span> <span class="kd">let</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">drawer</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="k">while</span> <span class="p">(</span><span class="nx">n</span> <span class="o">&amp;&amp;</span> <span class="nx">n</span> <span class="o">!==</span> <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">)</span> <span class="p">{</span> <span class="nx">chain</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="sb">`</span><span class="si">${</span><span class="nx">n</span><span class="p">.</span><span class="nx">tagName</span><span class="si">}</span><span class="sb">.</span><span class="si">${</span><span class="nx">n</span><span class="p">.</span><span class="nx">className</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span> <span class="nx">n</span> <span class="o">=</span> <span class="nx">n</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="k">return</span> <span class="nx">chain</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">// 返回：[DIV.pagefind-ui__drawer, FORM.pagefind-ui__form, DIV.pagefind-ui]
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">// → drawer 是 form 的 child、不是 sibling
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1">// → grid-row 在 .pagefind-ui 上設、無法控制 form 的 child
</span></span></span></code></pre></div><p>→ 換方向：drawer 改 absolute、form 加 margin-bottom 留 spacer。</p>
<h3 id="範例-2selector-過寬命中無關元素">範例 2：selector 過寬命中無關元素</h3>
<p><strong>錯</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;.title&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;search-title&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1">// 命中 page header 的 .title、navbar 的 .title、結果卡的 .title — 全變色
</span></span></span></code></pre></div><p><strong>對</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">const</span> <span class="nx">root</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.pagefind-ui&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">root</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s1">&#39;:scope &gt; .results &gt; .result &gt; .title&#39;</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="s1">&#39;search-title&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">// 起點 = .pagefind-ui、範圍 = 確切三層、過濾 = 不需要（已精準）
</span></span></span></code></pre></div><hr>
<h2 id="自檢清單dogfooding">自檢清單（dogfooding）</h2>
<p>寫 CSS 規則或 JS query 前：</p>
<ul>
<li><input disabled="" type="checkbox"> 我有沒有量過真實 DOM tree（playwright <code>browser_evaluate</code> 或 DevTools）？</li>
<li><input disabled="" type="checkbox"> Selector 的「起點」明確嗎？是 document / 元件根 / 函式參數 / closest 哪一個？</li>
<li><input disabled="" type="checkbox"> Selector 的「範圍」明確嗎？是 <code>&gt;</code> 直接子還是空格子孫？</li>
<li><input disabled="" type="checkbox"> Selector 的「過濾」明確嗎？需要 idempotency 標記嗎？</li>
<li><input disabled="" type="checkbox"> 過寬的 selector（<code>document.querySelectorAll('*')</code>、<code>[class*=&quot;x&quot;]</code>）能不能換成更精準的？</li>
</ul>
<p>任一項打勾失敗 → 補上、再寫規則。</p>
<hr>
<h2 id="延伸閱讀">延伸閱讀</h2>
<p>對應的事後檢討（在 <code>content/report/</code>）：</p>
<ul>
<li><a href="/blog/report/dom-topology-before-css/" data-link-title="拓樸理解先行於 CSS 規則" data-link-desc="寫 CSS 之前看真實 DOM tree、不靠 class name 推測層級。本文以『drawer 在 form 內、不是 form 的 sibling』這個假設錯誤為例，展開『拓樸理解 → CSS 規則』的順序。">dom-topology-before-css</a> — 拓樸理解先行於 CSS 規則</li>
<li><a href="/blog/report/dom-selector-precision/" data-link-title="Selector 精準度：讓 query 只命中你想要的元素" data-link-desc="JS 的 DOM query 是 sanity 防線、不是優化選項。從『起點 / 範圍 / 過濾』三層收斂、避免誤命中、避免未來頁面結構變動讓 query 撈到不該撈的東西。本文是 selector 設計的完整指引。">dom-selector-precision</a> — Selector 精準度三維度</li>
<li><a href="/blog/report/pattern-document-query/" data-link-title="Pattern：Document 全文件 query" data-link-desc="`document.querySelector` 從整個頁面找元素 — 是探索期與一次性 script 的合理工具、不是 production 客製的預設。本文展開這個 pattern 的適用邊界。">pattern-document-query</a> / <a href="/blog/report/pattern-component-root/" data-link-title="Pattern：元件根變數 query" data-link-desc="把元件根 `var shell = document.querySelector(&#39;.shell&#39;)` 一次存變數、之後所有 query 從 shell 開始 — 是 production 客製的預設起點。本文展開這個 pattern 的設計細節與邊界。">pattern-component-root</a> / <a href="/blog/report/pattern-root-as-parameter/" data-link-title="Pattern：起點當函式參數" data-link-desc="把元件根當函式參數傳入 — `function setup(shell) { shell.querySelector(...) }`、外部呼叫 `forEach(setup)` 處理多實例。本文展開純函式設計與多實例支援的取捨。">pattern-root-as-parameter</a> / <a href="/blog/report/pattern-closest-lookup/" data-link-title="Pattern：closest 反向找根" data-link-desc="事件處理時用 `e.target.closest(&#39;.shell&#39;)` 從事件目標反向找元件根 — 適合動態元件、SPA 路由切換、事件委派場景。本文展開反向定位 pattern 的應用邊界。">pattern-closest-lookup</a> — 起點四選一 pattern 卡片</li>
<li><a href="/blog/report/pattern-attribute-idempotency-marker/" data-link-title="Pattern：DOM attribute idempotency 標記" data-link-desc="用 `:not([data-x])` 過濾 &#43; 處理後 `setAttribute(&#39;data-x&#39;, &#39;true&#39;)` 保證每元素只處理一次 — 是 production apply 函式的預設 idempotency 工具。本文展開命名、生命週期、跟 framework 共處的設計細節。">pattern-attribute-idempotency-marker</a> / <a href="/blog/report/pattern-weakmap-idempotency-record/" data-link-title="Pattern：WeakMap idempotency 紀錄" data-link-desc="用 `WeakMap` 紀錄已處理的元素 — 不污染 DOM、適合第三方 library、跟 framework 衝突場景。本文展開 GC 行為、debug 替代方案、跟 attribute 標記的取捨。">pattern-weakmap-idempotency-record</a> — Idempotency 兩選一</li>
</ul>
<hr>
<p><strong>Last Updated</strong>: 2026-04-26
<strong>Version</strong>: 0.1.0</p>
]]></content:encoded></item></channel></rss>