← JavaScript Learning Path

JAVASCRIPT V2 · BROWSER · 41

DOM / Event / Render:JavaScript 怎麼真的改變畫面?

前面你已經會處理 value、array、object 與 function。現在第一次把這些 state 接到 Browser UI。這一課的核心不是背 querySelector,而是建立一條可以反覆使用的模型:user event → callback → state transition → render → DOM → screen。

Learning outcomes

1. Why now:JavaScript 的 value 終於要離開 Console

如果 Grade Tracker 只會在 Console 印出學生資料,它還不是一個使用者能操作的 app。Browser 程式要建立雙向資料流:

User input
  ↓
Browser event
  ↓
JavaScript state
  ↓
render()
  ↓
DOM
  ↓
screen

這條路徑會一路延伸到 React、Vue、Android UI,甚至桌面應用。框架只是把相同問題抽象化。

2. HTML source ≠ live DOM

<ul id="students"></ul>

Browser parse HTML 後建立 DOM objects。JavaScript 改的是目前 document 的 DOM state,不是把硬碟上的 HTML 檔案直接改掉。

const list =
  document.querySelector("#students");

list.textContent = "Loaded";
HTML textparseDOM treeJS mutationupdated DOMlayout/paintscreen

3. Selector 是一個查詢,不是一張保證書

const saveButton =
  document.querySelector("#save");

console.log(saveButton);

querySelector() 找不到時回 null。所以:

document
  .querySelector("#missing")
  .textContent = "Saved";

// TypeError:
// cannot read properties of null

第一個 debugging 問題不是「Browser 壞了嗎」,而是 selector 是否真的命中 live DOM。

4. Worked Example A:從 state render 一個值

const state = {
  name: "Anson",
  score: 90
};

function render() {
  document.querySelector("#name")
    .textContent = state.name;

  document.querySelector("#score")
    .textContent = state.score;
}

render();

這裡 state 是 canonical JavaScript data;DOM 是 state 的一種呈現。之後如果 score 變成 95,應該先更新 state,再 render。

5. Event listener:初始化時登記,事件發生後才執行

const button =
  document.querySelector("#save");

button.addEventListener(
  "click",
  event => {
    console.log(
      "clicked",
      event.type
    );
  }
);
時間發生什麼
script 初始化callback 被註冊給 Browser event system
等待中callback 沒有一直執行
使用者 clickBrowser 建立 event object
事件 dispatch對應 callback 才執行

6. Worked Example B:Button → State → Render

const state = {
  saved: false
};

const status =
  document.querySelector("#status");

const save =
  document.querySelector("#save");

function render() {
  status.textContent =
    state.saved
      ? "Saved"
      : "Not saved";

  status.classList.toggle(
    "saved",
    state.saved
  );
}

save.addEventListener(
  "click",
  () => {
    state.saved = true;
    render();
  }
);

render();

這就是本頁 mastery lab 真正在驗的能力:不是 source 裡「有 click 字樣」,而是 click 後 DOM 的 observable behavior 真的改變。

7. Form boundary:HTML input value 進 JavaScript 時仍要 normalize

form.addEventListener(
  "submit",
  event => {
    event.preventDefault();

    const name =
      nameInput.value.trim();

    const score =
      Number(scoreInput.value);

    console.log({
      name,
      score,
      scoreType: typeof score
    });
  }
);

input.value 是 string。第 36 / 57 課的 type normalization 在 Browser 這裡真正派上用場。

8. Worked Example C:Array state → List DOM

const students = [];

function render() {
  list.innerHTML =
    students
      .map(student => `
        <li>
          ${student.name}
          — ${student.score}
        </li>
      `)
      .join("");
}

第 58 課的 map() 現在不只是 array 題目,而是把 data model transform 成 UI representation。

9. State ownership:不要讓 DOM 成為唯一資料庫

// fragile idea
const score =
  Number(
    document
      .querySelector("#score")
      .textContent
  );

如果每次 business logic 都反向從畫面讀資料,你會很難知道真正 state 在哪。初學階段先採簡單原則:domain data 放 JavaScript state;DOM 主要負責呈現與收 input。

10. Common mistakes

A. Script 太早執行,selector 得到 null

確認 script 載入時機、defer、DOM 是否已存在。

B. 忘記 preventDefault

Form submit 造成 navigation/reload,看起來像 state「莫名消失」。

C. State 更新了但沒有 render

Console 看得到新 array,畫面卻沒變。

D. Render 只 append、不重建或去重

每次操作都把同樣內容重複加進 DOM。

11. Debug evidence:依資料流找第一個斷點

console.log("event fired");
console.log({ name, score });
console.log(students);
console.log(list);
  1. Event callback 有沒有進?
  2. Input value/type 正不正確?
  3. JavaScript state 有沒有更新?
  4. render() 有沒有被呼叫?
  5. Selector 是否拿到 element?
  6. DOM 有改但 CSS 隱藏嗎?

12. Guided exercise:Predict → Click → Observe

let count = 0;

button.addEventListener(
  "click",
  () => {
    count += 1;
    output.textContent = count;
  }
);

先回答:初始化後 output 是什麼?第一次 click 後 count / DOM 各是多少?第三次 click 後呢?再用 DevTools Console 與 Elements 驗證。

13. Independent exercise:完成 Save State

建立 #status 與 #save。初始顯示 Not saved;click 後顯示 Saved 並加上 saved class。不要用 inline onclick,使用 addEventListener。

14. Challenge:加入可重複新增的 Student Form

把 name/score form 接到 students array。要求:trim name、score 轉 number、拒絕空 name、submit 後 render list、reset form。思考哪一段是 input boundary,哪一段是 domain state,哪一段是 presentation。

Project checkpoint:Grade Tracker Browser Shell

Form
  ↓ submit
normalize input
  ↓
Student object
  ↓ push
students[]
  ↓
render()
  ↓
DOM list

完成這個 checkpoint 後,你已經有一個純 Browser app。下一步不是立刻上框架,而是把資料來源從 local memory 接到 HTTP/API。

15. Vocabulary / Summary

16. Further reading

Knowledge check

  1. 解釋 HTML source 與 live DOM 的差別。
  2. 說明 querySelector() 為何可能回 null。
  3. 畫出 event → state → render → DOM。
  4. 解釋 input.value 為何仍需要 type normalization。
  5. 完成本頁 Live DOM mastery checks。