<?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>Selector on Tarragon</title><link>https://tarrragon.github.io/blog/tags/selector/</link><description>Recent content in Selector 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/selector/index.xml" rel="self" type="application/rss+xml"/><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>