最小必要範圍是 sanity 防線:保護行為可預測性
核心原則
「最小必要範圍」是 sanity 防線、不是優化選項。 縮 selector / observer / DOM 操作的範圍、目的不是為了讓程式跑更快、是為了讓行為可預測:不誤命中、不過度觸發、不被未來頁面結構變動打破。從具體放寬比從寬泛收緊容易得多 — 兩者的成本曲線完全不對稱。
為什麼是「sanity 防線」、不是「優化」
兩個概念常被混為一談
「縮範圍」聽起來像效能優化(少做一點工 = 快一點)— 這誤解掩蓋了它真正的價值。
| 維度 | 優化 | Sanity 防線 |
|---|---|---|
| 目標 | 提升某個量化指標(速度、記憶體) | 防止某類錯誤發生 |
| 衡量 | 跑得多快、用多少資源 | 行為是否可預測 |
| 失敗代價 | 慢一點 | 出錯時 debug 困難 |
| 該追求的時機 | 有量到瓶頸時 | 寫第一行就該追求 |
把「縮範圍」當優化的後果:以為「現在沒效能問題、之後再縮」 — 但 sanity 防線錯過了第一行就追求的時機、未來補救成本更高。
寬範圍的代價不是「慢」
寬 selector / observer / 操作範圍的失敗模式:
| 失敗 | 表現 | Debug 難度 |
|---|---|---|
| 誤命中其他元素 | 動了不該動的、且通常不報錯 | 高 — 安靜失敗、bug 表現遠離 root cause |
| 過度觸發 | apply 跑了 N 次、其中 N-1 次無意義 | 中 — 看 callstack 不知道為什麼觸發 |
| 跟未來結構變動衝突 | 加了一個 widget 後原本的程式壞掉 | 高 — 不知道哪個假設被打破 |
| 跟 framework 渲染週期競爭 | 在 layout 還沒穩時跑、視覺閃爍 | 高 — 時序問題、難以重現 |
四種代價都不是「慢」 — 都是「行為不可預測」。Sanity 防線守的是這個。
「從具體放寬」vs「從寬泛收緊」的不對稱性
兩個方向在表面上對稱、實際成本曲線完全不對稱:
從具體放寬(推薦)
1寫第一版:用最具體的 selector / 最小的 observer 範圍 / 最窄的操作邊界
2↓
3發現某個 case 沒覆蓋
4↓
5顯式評估「該放寬到什麼程度」
6↓
7擴大範圍、知道擴大後的影響範圍每次擴大都是顯式決定 — 知道「我為什麼擴大、擴大到哪」。
從寬泛收緊(反推薦)
1寫第一版:用最寬的 selector / subtree observer / 全頁面操作
2↓
3某天發現某個 bug(誤命中、過度觸發、framework 衝突)
4↓
5不確定哪些地方是「故意要寬」、哪些是「意外寬了」
6↓
7試著縮、可能漏掉某些故意要寬的場景、引發新 bug收緊時面對的問題:寬範圍的程式碼裡看不出「哪些是故意寬、哪些是意外寬」。原作者也不一定記得當初為什麼寫寬。
不對稱性的根源
這個不對稱不是工程偏好、是資訊量的差異:
- 從具體放寬:每次擴大時、有當前需求當證據(「為了 X case 才擴大」)
- 從寬泛收緊:縮的時候、不知道原本依賴哪些寬範圍特性
寫程式時的「具體 → 寬泛」走法保留了決策軌跡;「寬泛 → 具體」走法丟失了軌跡。
三類範圍的共同骨架
「最小必要範圍」原則跨三類獨立議題:
| 議題 | 範圍對象 | 失敗模式 | 對應實作篇 |
|---|---|---|---|
| JS 元件邊界 | 「我可以動什麼」的契約 | 越界操作 framework 管的部分、被重繪清掉 | #13 元件邊界與 JS 操作的影響範圍 |
| Selector 精準度 | 「query 命中哪些元素」 | 誤命中、未來結構變動就壞 | #14 Selector 精準度 |
| Observer 範圍 | 「監聽哪些變動」 | 過度觸發、layout 抖動、無限循環 | #29 MutationObserver 範圍與觸發頻率 |
三者表現不同、機制不同、但底層都是同一條原則的應用 — 越寬越脆弱、越具體越穩定。
共通的設計工具
跨三類議題、設計「最小必要範圍」的工具有共通模式:
| 維度 | 三類議題的對應 |
|---|---|
| 起點 / 邊界 | JS:元件邊界契約;Selector:query 起點;Observer:root |
| 深度 | JS:操作層級;Selector:是否找深層;Observer:subtree |
| 過濾 | JS:操作前界定;Selector:attribute filter / :not;Observer:option flag |
每個議題都有「起點 / 深度 / 過濾」三維度可顯式設計 — 同樣的設計骨架在不同情境重現。
應用辨識訊號
下次工作中、看到這些訊號該想到「最小必要範圍」:
| 訊號 | 對應議題 | 行動 |
|---|---|---|
| 「我先寫寬一點、之後有問題再縮」 | 任一類 | 反向、第一版就寫具體 |
| 「現在只一個元件、document.query 也行」 | Selector 起點 | 用元件根變數、預防未來擴展 |
| 「subtree: true 比較保險」 | Observer 範圍 | 縮到實際關心的子節點 |
| 「先 framework 內注入個 element 看看」 | JS 元件邊界 | 留在 framework 邊界外 |
| 「同樣的 bug 出現在不同元件」 | 任一類 | 範圍寬了、影響跨越元件邊界 |
| 「改了 X、Y 跟 Z 也壞」 | 任一類 | 範圍寬了、改動波及 |
不該套用「最小必要範圍」的情境
這條原則有適用邊界、不是所有「縮」都有意義:
| 情境 | 為什麼不套用 |
|---|---|
| 探索 / debug 階段 | 寬範圍幫助觀察全貌、確認問題範圍後再縮 |
| 一次性 script、跑完就丟 | 沒有「未來變動」的問題、簡潔優先 |
| 確實需要看深層變動的 observer | subtree: true 是必要、不是 over-broad |
| 確實要對全頁套用的操作(theme 切換) | 全頁面才是「最小必要範圍」 |
核心判準:「最小必要範圍 = 滿足當前需求的最窄範圍」 — 不是極致最小、是「不再小就會漏」的點。盲目縮到「比需要還小」會犧牲覆蓋率、是另一種錯誤。
跟「2 次門檻」的協同
#42 2 次門檻 處理「失敗第 N 次該換策略」、本原則處理「第一次設計時範圍該多大」。兩者方向互補:
- 2 次門檻:失敗發生後、何時該升級處理層級
- 最小必要範圍:寫第一版時、就該追求 sanity 防線
如果第一版就遵循「最小必要範圍」、後續觸發 2 次門檻的機率會降低 — sanity 防線是預防、2 次門檻是補救。
對應的實作篇
每篇示範這個原則在不同議題的應用:
| 篇 | 議題 | 範圍對象 |
|---|---|---|
| #13 元件邊界與 JS 操作的影響範圍 | JS 元件邊界 | 「我可以動什麼」契約 |
| #14 Selector 精準度 | DOM query 範圍 | 起點 / 範圍 / 過濾三維度 |
| #29 MutationObserver 範圍與觸發頻率 | Observer 監聽範圍 | root / option / 頻率三維度 |
讀的時候從本篇出發、依議題挑實作篇。
判讀徵兆
| 訊號 | 自問 | 回應 |
|---|---|---|
| 「之後有問題再縮」 | 縮的時候會知道哪些是故意寬嗎? | 否 → 第一版就寫具體 |
| 「以防萬一勾 subtree」 | 真的有深層變動需要監聽嗎? | 否 → 移除 subtree |
| 「document.query 比較簡單」 | 未來頁面會不會有第二個同名元素? | 不確定 → 用元件根變數 |
| 「怕 selector 太窄漏掉」 | 漏掉時會怎樣、可以擴大嗎? | 可以 → 從具體開始、漏了再擴 |
| Bug 表現「不知道哪改的」 | 範圍是否寬了、波及不該動的地方? | 是 → 縮範圍 |
核心原則:最小必要範圍守護的是「行為可預測」 — 寫的時候多想一點、debug 時少痛一點。寬範圍的代價不是慢、是出錯時定位困難 — 這也是為什麼這條原則在「沒效能瓶頸」的情境下仍然成立。
延伸到 stream 操作:#64 Feature 操作要跟 Source 同層合成 是本原則在 stream 領域的應用 — 「合成位置」就是「操作的範圍邊界」、選錯位置 = 範圍錯。寬範圍便利、窄範圍對齊、兩者反相關的更高層原則見 #67 寫作便利度跟意圖對齊反相關。