EP.15Web 開發系列

不用 React,做個動態待辦清單
——然後你就懂了

從「手動操作 DOM 的痛苦」理解 React 為什麼存在。 先用原生 JavaScript 體驗那個痛點,再看 React 如何優雅地解決它。

Joseph Chen 2026 12 min read React · DOM · Virtual DOM · 為什麼學 React

「讓我先問你一個問題:如果有人說用 HTML + CSS + JavaScript 就能做網頁,為什麼還需要 React?」

這篇帶你親手感受那個痛點。我們先用純 JavaScript 做一個待辦清單, 讓你真實體會「手動操作 DOM」有多麻煩——然後你就會懂 React 為什麼存在了。

先做一個原生 JS 待辦清單

先別碰 React。我們用最原始的 HTML + JavaScript 來做一個能「新增、勾選完成、刪除」的待辦清單。 這個版本完全可以運作——先看它怎麼寫的:

vanilla-todo.html — 純 HTML + JavaScript 版本
<!DOCTYPE html>
<html>
<body>
  <input id="input" placeholder="輸入待辦事項">
  <button onclick="addTodo()">新增</button>
  <ul id="list"></ul>
  <p><span id="count">0</span> 筆待辦</p>

  <script>
    const todos = [];

    function addTodo() {
      const input = document.getElementById('input');
      const value = input.value;
      if (!value) return;

      todos.push({ id: Date.now(), text: value, done: false });
      input.value = '';
      render();  // 每次更新都要手動呼叫
    }

    function toggleTodo(id) {
      const todo = todos.find(t => t.id === id);
      todo.done = !todo.done;
      render();  // 又要手動呼叫
    }

    function deleteTodo(id) {
      const index = todos.findIndex(t => t.id === id);
      todos.splice(index, 1);
      render();  // 還是手動呼叫
    }

    function render() {
      // 每次都要整個重新建 DOM
      const list = document.getElementById('list');
      list.innerHTML = '';  // 先清空

      todos.forEach(todo => {
        const li = document.createElement('li');
        li.innerHTML = `
          <span style="text-decoration: ${todo.done ? 'line-through' : 'none'}">${todo.text}</span>
          <button onclick="toggleTodo(${todo.id})">完成</button>
          <button onclick="deleteTodo(${todo.id})">刪除</button>
        `;
        list.appendChild(li);
      });

      // 別忘了更新計數器!
      document.getElementById('count').textContent = todos.length;
    }
  </script>
</body>
</html>

這個版本「可以用」。功能都有:新增、完成、刪除、計數器。 但等等——你有沒有注意到 render() 出現了三次? 而且每次改資料之後,你都要記得手動呼叫它。


這裡有什麼問題?

乍看之下沒什麼大問題。但等到你要加功能——「按重要度排序」、「只顯示未完成」、「每筆待辦有截止日」—— 問題就開始浮現。讓我們拆開來看:

1

每次都要手動更新 DOM

你改了資料(todos 陣列), 畫面不會自動更新——你必須記得呼叫 render()。 如果你忘了,畫面和資料就不同步了。功能越多,需要呼叫 render() 的地方越多,越容易漏掉。

2

render() 效率差

每次 render() 都先 list.innerHTML = '' 清空整個列表,再重建所有項目。 你點了一個「完成」按鈕,結果整個列表都重建了——包括那些根本沒變的待辦事項。 清單 100 筆,你改一筆,100 筆全部重新建 DOM。

3

程式碼越來越難維護

加一個「按重要度排序」功能,你需要在十幾個地方更新邏輯,還要記得呼叫 render()。 「只顯示未完成」功能也一樣。每加一個功能,複雜度就跳一個量級。 六個月後換人接手(或六個月後的自己接手),根本看不懂資料和畫面之間的關係。

Excel 比喻

就像你在 Excel 裡用公式 SUM,但每次改了數字, 你要手動按 F9 才會更新——這不對,公式就應該自動計算。 React 就是讓畫面能「自動計算」。你只需要說「這格的值是什麼」, 計算的事讓工具幫你做。


React 怎麼解決這個問題

把剛才的待辦清單改寫成 React 版本。對比著看,你就會懂核心差異在哪:

TodoApp.tsx — React 版本
import { useState } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  function addTodo() {
    if (!input) return;
    setTodos([...todos, { id: Date.now(), text: input, done: false }]);
    setInput('');
    // ← 不需要手動 render(),React 自動處理!
  }

  function toggleTodo(id) {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
    // ← 只更新這個項目,其他不動
  }

  function deleteTodo(id) {
    setTodos(todos.filter(todo => todo.id !== id));
  }

  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <button onClick={addTodo}>新增</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => toggleTodo(todo.id)}>完成</button>
            <button onClick={() => deleteTodo(todo.id)}>刪除</button>
          </li>
        ))}
      </ul>
      <p>{todos.length} 筆待辦</p>
    </div>
  );
}

原生 JS 版本的負擔

  • 每次改資料都要手動呼叫 render()
  • render() 清空整個 DOM 再重建
  • 資料和 DOM 操作混在一起
  • 計數器要單獨記得更新

React 版本的差異

  • setTodos() 之後畫面自動更新
  • React 只更新有差異的部分
  • 你只管資料,不管 DOM
  • 計數器 {todos.length} 自動同步

React 的核心思想

你只需要說明資料是什麼(todos), 以及怎麼更新資料(setTodos)。 畫面的事,React 幫你搞定。

UI = f(data)

UI 是資料的函式。資料變了,UI 自動跟著變。


Virtual DOM——React 為什麼快

你可能會想:「React 畫面自動更新,那它不是每次都重建整個 DOM 嗎?那不是更慢?」 答案是——React 用了 Virtual DOM,讓更新變得聰明很多。

裝修房子的比喻

想像你要裝修一間房子。

傳統做法(原生 JS):每次有任何改動,就把整個房子拆掉重建。換個燈泡也拆整棟。

React 的做法:先在腦袋裡(Virtual DOM)規劃好改動,比對前後差異, 只搬那幾件家具、換那顆燈泡,其他不動。不拆整間房子。

原生 JS 更新流程

資料改變innerHTML = '' 清空整個 DOM 重建瀏覽器重新繪製

React 更新流程

資料改變計算新 Virtual DOM比對 diff只更新有差異的部分

Virtual DOM 是 React 在記憶體中維護的一份 DOM 樹的輕量描述。 每次狀態改變,React 先更新 Virtual DOM,再比較前後兩棵樹的差異(reconciliation), 最後只把真正有變化的部分同步到瀏覽器的實際 DOM。 這個過程叫做 Diffing


React 的三個核心心法

React 不只是一個「讓 DOM 操作變簡單」的工具,它背後有一套設計哲學。 理解這三個心法,你學 React 的速度會快很多:

1

UI = f(state)

把注意力放在「資料」,不是「DOM 操作」。你只需要描述「當資料是 X 時,畫面長什麼樣子」。 React 的工作是把這個描述轉換成真實的 DOM 並保持同步。 你不再需要思考「我要操作哪個 DOM 元素」——只需要思考「資料應該長什麼樣子」。

2

單向資料流

資料從上往下傳(父組件 → 子組件),只能透過 Props 傳遞。 這讓你永遠知道「這個資料從哪來」——不會有資料從四面八方湧入、你搞不清楚誰改了什麼的狀況。 單向流讓 debug 變得容易:追蹤資料的源頭,就能找到問題所在。

3

組件化(Component)

把 UI 拆成獨立的積木,每個積木只管自己的邏輯。<TodoItem /> 只管一筆待辦事項的顯示,<TodoList /> 只管清單的排列,<TodoApp /> 管整個應用的狀態。 每個積木可以獨立開發、測試、重複使用。


本篇重點回顧

01原生 JS 的問題:資料改了,你必須手動呼叫 render() 更新 DOM。功能越多,越容易忘記、越難維護。
02原生 JS 的 render() 每次清空整個 DOM 再重建,效率差。React 用 Virtual DOM + Diffing,只更新有變化的部分。
03React 的核心哲學:UI = f(data)。你只管資料(state),畫面自動同步,不需要手動操作 DOM。
04三個心法:宣告式 UI(描述結果)、單向資料流(來源清晰)、組件化(可重複使用的積木)。

現在你知道為什麼需要 React 了。下一篇就直接進去:Component、JSX、Props、State——React 的四個核心積木。


ReactJavaScriptDOMVirtual DOM為什麼學 ReactEP.15