LESSON-16 · 自主延伸

安全 DOM 查找與可重複渲染

把「安全 DOM 查找與可重複渲染」拆成 4 個可獨立完成、可重設、可驗證的自主學習 module。

1. 現在要做什麼

成果:完成具 null guard、data/class 狀態且重跑不重複的安全清單 renderer。

需要的檔案:files/starter/index.html(整合起點,保留跨 module 的最小缺口。)

完成後:完成具 null guard、data/class 狀態且重跑不重複的安全清單 renderer。

0 / 8 個操作已記錄

2. 先備自我檢查

逐項展開;答不出來時先走補救連結,不必硬做。

已完成本章基礎練習並能找到 Starter。

自我檢查:我能否指出本章 Starter 入口與唯一 TODO?

不足時補救:回到本章 README,先完成基礎練習與入口導覽。

能開啟本機 HTML 並讀取至少一種瀏覽器證據。

自我檢查:我能否開啟 Elements、Console 或 Network 並找到本頁?

不足時補救:先開 DevTools,重新整理一次,確認目前文件與第一筆 request。

3. 完整學習目標與 Learning Map

每一個目標只對應一個 module;成功狀態必須能指出證據。

能處理 querySelectorAll 的 NodeList,並在單一 selector 找不到時安全停止。

為什麼重要
把 NodeList 當單一元素或對 null 讀 property,會讓整段 render 在第一個缺少節點時中止。
完成條件
查找三張卡與一個故意不存在的 status,先判斷 null 再處理。
成功狀態
itemCount=3、missing=true、guarded=true、uncaught=0。
證據位置
itemCount=3、missing=true、guarded=true

能以 dataset 讀 DOM 識別,並用 classList 切換呈現狀態。

為什麼重要
把狀態塞進完整 className 字串容易覆蓋既有類別;data-* 則提供 HTML/JS 都可讀的穩定識別。
完成條件
讀 data-id=s2,toggle is-selected 並檢查 contains。
成功狀態
dataId=s2、before=false、after=true、其他 class 保留。
證據位置
dataId=s2、selected=true、baseClass=true

能先在 DocumentFragment 組裝三個節點,再一次加入正式 DOM。

為什麼重要
把建立與插入分開可讓 render 流程清楚,並避免每筆資料都直接操作 live container。
完成條件
建立三個 li 加入 fragment,記錄插入前 fragment child count,再 append。
成功狀態
fragmentBefore=3、domAfter=3、fragmentAfter=0、appendCalls=1。
證據位置
fragmentBefore=3、domAfter=3、fragmentAfter=0

能讓相同資料連續 render 兩次後節點數仍等於資料數。

為什麼重要
搜尋、resize 或狀態更新會重跑 render;若只 append,清單會累積重複內容。
完成條件
render 每次先 replaceChildren,再加入同一三筆資料;連跑兩次。
成功狀態
firstCount=3、secondCount=3、duplicateIds=0。
證據位置
firstCount=3、secondCount=3、duplicateIds=0

4. 4 個知識模組與 Micro Labs

依序展開;每次只改一項,操作後確認該模組自己的 DOM、progress 與 evidence。

1querySelectorAll() 與 null guard

完整解釋

白話:找多個先看有幾個;找一個可能找不到,先確認不是 null。

正式說法:querySelectorAll 回傳靜態 NodeList;querySelector 無匹配時回傳 null,存取前需控制流程縮小型別。

何時使用:批次卡片、導覽連結與不是每頁都有的可選元件。

修改前後:Before:missing.textContent 直接 TypeError。After:null guard 讓其他 render 繼續。

常見混淆:空 NodeList 不是 null,仍可 forEach;querySelector 才可能直接回 null。

名詞與最小範例

querySelectorAll() 與 null guard Multiple-element query and null guard

白話
找多個先看有幾個;找一個可能找不到,先確認不是 null。
正式定義
querySelectorAll 回傳靜態 NodeList;querySelector 無匹配時回傳 null,存取前需控制流程縮小型別。
何時使用
批次卡片、導覽連結與不是每頁都有的可選元件。
最小範例
const status=document.querySelector('#status'); if (!status) return。
驗收證據
itemCount=3、missing=true、guarded=true
常混淆
空 NodeList 不是 null,仍可 forEach;querySelector 才可能直接回 null。

Micro Lab|querySelectorAll() 與 null guard

情境:共用 main.js 在沒有 status 的頁面整段白畫面。

起始狀態:程式直接對 missing selector 讀 textContent。

  1. 查三個 items 與缺少的 status,以 guard 記錄安全分支。
  2. 執行下方操作,觀察獨立的 DOM 與資料狀態。
  3. 依證據欄位重新驗收。

目前主題:querySelectorAll() 與 null guard

A
B
C
尚未執行;DOM data-state=starter。

預期:itemCount=3、missing=true、guarded=true、uncaught=0。

證據:itemCount=3、missing=true、guarded=true

第一個檢查:先輸出 selector 結果與 NodeList.length。

重設:清空輸出並保留三張卡、缺少 status。

卡住時

  • 症狀:Console 顯示 Cannot read properties of null。
    可能原因:selector 無匹配仍直接讀 property。
    先檢查:先 console.log 該 querySelector 結果。
    修正:加入 null guard 或修正 selector/頁面契約。

理解檢查

querySelectorAll 無匹配時回什麼?

尚未作答。

查看解釋

它仍是可讀 length=0 的集合。

本模組官方來源:MDN|Document.querySelectorAll()

2classList 與 dataset

完整解釋

白話:data-* 放元素資料;classList 只加減一個狀態 class,不重寫全部 class。

正式說法:dataset 映射 data-* attributes;classList 提供 DOMTokenList 的 add/remove/toggle/contains。

何時使用:卡片選取、展開狀態與 DOM 上的資料 id。

修改前後:Before:element.className='selected' 遺失 card。After:card 與 is-selected 同時存在。

常見混淆:dataset 值都是字串,不是自動轉成 number/Boolean。

名詞與最小範例

classList 與 dataset Class list and data attributes

白話
data-* 放元素資料;classList 只加減一個狀態 class,不重寫全部 class。
正式定義
dataset 映射 data-* attributes;classList 提供 DOMTokenList 的 add/remove/toggle/contains。
何時使用
卡片選取、展開狀態與 DOM 上的資料 id。
最小範例
card.dataset.id;card.classList.toggle('is-selected')。
驗收證據
dataId=s2、selected=true、baseClass=true
常混淆
dataset 值都是字串,不是自動轉成 number/Boolean。

Micro Lab|classList 與 dataset

情境:點選卡片後原本布局 class 被 className 覆蓋。

起始狀態:card class 存在,is-selected 不存在。

  1. 讀 data-id,切換 is-selected 並確認既有 card class 未消失。
  2. 執行下方操作,觀察獨立的 DOM 與資料狀態。
  3. 依證據欄位重新驗收。

目前主題:classList 與 dataset

尚未執行;DOM data-state=starter。

預期:dataId=s2、before=false、after=true、其他 class 保留。

證據:dataId=s2、selected=true、baseClass=true

第一個檢查:先讀 classList.value 與 dataset.id。

重設:移除 is-selected,保留 data-id 與 card。

卡住時

  • 症狀:點擊後卡片失去所有原樣式。
    可能原因:以 className 覆蓋整個字串。
    先檢查:比較操作前後 classList.value。
    修正:改用 classList.add/remove/toggle。

理解檢查

dataset 讀出的值預設是什麼型別?

尚未作答。

查看解釋

需要數值或 Boolean 時要明確轉型。

本模組官方來源:MDN|Element.classList

3DocumentFragment

完整解釋

白話:先在看不見的暫存盒裝好三項,再一次放進畫面。

正式說法:DocumentFragment 是無 parent 的輕量 Document 節點;append 時其 children 會移入目標,fragment 隨後變空。

何時使用:從陣列建立多個 DOM 節點並批次提交到容器。

修改前後:Before:每建一項就 append container。After:先填 fragment,再單次 append。

常見混淆:fragment 不是複製節點;append 會移動其 children。

名詞與最小範例

DocumentFragment Document fragment batch insertion

白話
先在看不見的暫存盒裝好三項,再一次放進畫面。
正式定義
DocumentFragment 是無 parent 的輕量 Document 節點;append 時其 children 會移入目標,fragment 隨後變空。
何時使用
從陣列建立多個 DOM 節點並批次提交到容器。
最小範例
const fragment=document.createDocumentFragment(); fragment.append(li); list.append(fragment)。
驗收證據
fragmentBefore=3、domAfter=3、fragmentAfter=0
常混淆
fragment 不是複製節點;append 會移動其 children。

Micro Lab|DocumentFragment

情境:render 迴圈同時負責建立與插入,難以檢查中間結果。

起始狀態:空 list 與三筆文字資料。

  1. 把三項加入 fragment,再一次 append 到 list。
  2. 執行下方操作,觀察獨立的 DOM 與資料狀態。
  3. 依證據欄位重新驗收。

目前主題:DocumentFragment

    尚未執行;DOM data-state=starter。

    預期:fragmentBefore=3、domAfter=3、fragmentAfter=0、appendCalls=1。

    證據:fragmentBefore=3、domAfter=3、fragmentAfter=0

    第一個檢查:先在 append 前記錄 fragment.childElementCount。

    重設:replaceChildren 清空 list。

    卡住時

    • 症狀:append 後 fragment count 仍期待為 3。
      可能原因:不了解 fragment children 會被移動。
      先檢查:同時記錄 list 與 fragment child count。
      修正:以 domAfter 驗收完成節點,不把空 fragment 當失敗。

    理解檢查

    fragment append 到 DOM 後自身 children 如何?

    尚未作答。

    查看解釋

    append 移動節點,不是 clone。

    本模組官方來源:MDN|DocumentFragment

    4可重複執行且不重複節點的 render

    完整解釋

    白話:每次重畫先把舊結果替換掉,不是一直往後加。

    正式說法:本練習的 idempotent render 對相同 input 重複執行後產生等價 DOM state。

    何時使用:篩選、排序、重新抓資料或任何可能重跑的列表 renderer。

    修改前後:Before:第二次 render 變六張卡。After:兩次都維持三張。

    常見混淆:idempotent 不表示函式沒有 DOM 副作用;它表示重複套用後最終狀態不再改變。

    名詞與最小範例

    可重複執行且不重複節點的 render Idempotent DOM rendering

    白話
    每次重畫先把舊結果替換掉,不是一直往後加。
    正式定義
    本練習的 idempotent render 對相同 input 重複執行後產生等價 DOM state。
    何時使用
    篩選、排序、重新抓資料或任何可能重跑的列表 renderer。
    最小範例
    container.replaceChildren(fragment) 取代舊清單。
    驗收證據
    firstCount=3、secondCount=3、duplicateIds=0
    常混淆
    idempotent 不表示函式沒有 DOM 副作用;它表示重複套用後最終狀態不再改變。

    Micro Lab|可重複執行且不重複節點的 render

    情境:使用者每按一次篩選,服務卡數量就翻倍。

    起始狀態:空容器與固定三筆資料。

    1. 以相同資料連續 render 兩次並檢查 count 與 id 唯一性。
    2. 執行下方操作,觀察獨立的 DOM 與資料狀態。
    3. 依證據欄位重新驗收。

    目前主題:可重複執行且不重複節點的 render

    尚未執行;DOM data-state=starter。

    預期:firstCount=3、secondCount=3、duplicateIds=0。

    證據:firstCount=3、secondCount=3、duplicateIds=0

    第一個檢查:先找 render 是否只 append 而未清理/取代。

    重設:清空容器與 render counter。

    卡住時

    • 症狀:第二次 render 有六個節點。
      可能原因:只使用 append,未清理舊結果。
      先檢查:記錄每次 render 前後 childElementCount。
      修正:改用 replaceChildren 或先明確清空容器。

    理解檢查

    本課 idempotent render 的判準是什麼?

    尚未作答。

    查看解釋

    它仍會 render,但不累積重複節點。

    本模組官方來源:MDN|Element.replaceChildren()

    5. 安全 DOM 查找與可重複渲染|整合 Lab

    情境:在同一份章節作品中整合 4 個 module,排除單項通過但組合失敗的問題。

    交付:完成具 null guard、data/class 狀態且重跑不重複的安全清單 renderer。

    尚未執行整合步驟。
    1. 1觀察整合 Starter 並找出第一個未通過 module。

      檔案:files/starter/index.html 定位:#observe

      操作:逐項讀取 outcomes,執行一次現況操作。

      // OBSERVE:不修改程式,先記錄第一個失敗 moduleId。

      預期:能指出第一個未通過 module。

      證據:第一個 moduleId 與缺少的 evidence。

      原因:先觀察可避免未重現就直接改答案。

      卡住先查:確認開啟的是 files/starter/index.html。

    2. 2依 module 順序修改,一次只加入一個責任。

      檔案:files/starter/index.html 定位:#modify

      操作:查三個 items 與缺少的 status,以 guard 記錄安全分支。 → 讀 data-id,切換 is-selected 並確認既有 card class 未消失。 → 把三項加入 fragment,再一次 append 到 list。 → 以相同資料連續 render 兩次並檢查 count 與 id 唯一性。

      // 16-01-queryselectorall-null-guard: 查三個 items 與缺少的 status,以 guard 記錄安全分支。
      // 16-02-classlist-dataset: 讀 data-id,切換 is-selected 並確認既有 card class 未消失。
      // 16-03-documentfragment: 把三項加入 fragment,再一次 append 到 list。
      // 16-04-render: 以相同資料連續 render 兩次並檢查 count 與 id 唯一性。

      預期:完成具 null guard、data/class 狀態且重跑不重複的安全清單 renderer。

      證據:itemCount=3、missing=true、guarded=true;dataId=s2、selected=true、baseClass=true;fragmentBefore=3、domAfter=3、fragmentAfter=0;firstCount=3、secondCount=3、duplicateIds=0

      原因:逐項整合能把回歸定位到明確 module。

      卡住先查:回到第一個未通過的 module,只修該處。

    3. 3驗證所有 module 在整合後仍成立。

      檔案:files/solution/index.html 定位:#verify

      操作:重新整理並依 outcomes 順序重跑所有證據。

      // VERIFY:逐項記錄 pass/fail,不以看到畫面代替 evidence。

      預期:4/4 module 通過。

      證據:每個 moduleId 都有實際結果與重設後重現紀錄。

      原因:單項通過不代表整合後沒有 selector、id 或狀態衝突。

      卡住先查:若失敗,回到第一個失敗 module,不同時修多項。

    4. 4把本章責任轉用到不同內容情境。

      檔案:files/starter/index.html 定位:#challenge

      操作:替另一個商家情境重做,仍達成:完成具 null guard、data/class 狀態且重跑不重複的安全清單 renderer。

      // CHALLENGE:只替換內容與資料,不新增框架或跳過 self-check。

      預期:完成具 null guard、data/class 狀態且重跑不重複的安全清單 renderer。

      證據:轉用後的作品與全數 module self-check。

      原因:能轉用才代表理解責任,而非只記住原範例。

      卡住先查:先確認共同基底與 module id 未被改壞。

    重設方法:重新載入 files/starter/index.html;若 localStorage 不可用,使用頁面內記憶狀態並提供重設按鈕。

    6. 自學挑戰與三層提示

    挑戰:將同一組責任轉用到另一個商家情境,仍達成:完成具 null guard、data/class 狀態且重跑不重複的安全清單 renderer。

    限制:不新增框架。;保留 module id 與驗收證據。

    提交證據:提交可直接開啟的作品與逐項 self-check。

    提示 1|方向提示

    拆小:在「安全 DOM 查找與可重複渲染」先完成第一個尚未通過的 module:querySelectorAll() 與 null guard,不同時修改其他責任。

    提示 2|關鍵片段

    串接:依 16-01-queryselectorall-null-guard → 16-02-classlist-dataset → 16-03-documentfragment → 16-04-render 核對各自輸入與證據。

    提示 3|完整解答與原因

    完整解答與原因:依序執行 查三個 items 與缺少的 status,以 guard 記錄安全分支。 → 讀 data-id,切換 is-selected 並確認既有 card class 未消失。 → 把三項加入 fragment,再一次 append 到 list。 → 以相同資料連續 render 兩次並檢查 count 與 id 唯一性。;這個順序能把每個結果對回單一 module,避免整合後無法定位。

    完整解答預設收合;先留下自己的嘗試,再開第三層。

    7. 症狀式除錯指南

    8. 理解測驗、驗收證據與下一步

    哪個狀態代表 16-01-queryselectorall-null-guard 完成?

    尚未作答。

    查看解釋

    把 NodeList 當單一元素或對 null 讀 property,會讓整段 render 在第一個缺少節點時中止。

    哪個狀態代表 16-02-classlist-dataset 完成?

    尚未作答。

    查看解釋

    把狀態塞進完整 className 字串容易覆蓋既有類別;data-* 則提供 HTML/JS 都可讀的穩定識別。

    哪個狀態代表 16-03-documentfragment 完成?

    尚未作答。

    查看解釋

    把建立與插入分開可讓 render 流程清楚,並避免每筆資料都直接操作 live container。

    哪個狀態代表 16-04-render 完成?

    尚未作答。

    查看解釋

    搜尋、resize 或狀態更新會重跑 render;若只 append,清單會累積重複內容。

    驗收清單

    官方來源

    回到課程地圖選擇下一章