跳至主要内容

課程:JavaScript 與 React 底層原理 第 10 堂:React 核心設計哲學

30 命令式 vs 宣告式

想像你正在指揮一位廚師準備晚餐。

第一種方式,你站在他身邊,下達一連串精確的動作指令:「打開瓦斯爐」、「拿起平底鍋」、「倒入 20 毫升的橄欖油」、「等油溫到攝氏 180 度時放入洋蔥」、「每隔 5 秒翻炒一次」。如果其中一個步驟出錯——例如油溫不夠你就叫他放洋蔥,或者你忘記叫他關火——最後的成品就會與你的預期大相徑庭。

第二種方式,你直接給他一張完美的晚餐照片,並告訴他:「我今晚要吃成這副模樣的燉飯。」至於廚師是要先切洋蔥還是先熱鍋,油溫要如何控制,那都是廚師的事,他會根據這張照片(目標狀態)來決定所有的執行細節。

這正是 命令式(Imperative)宣告式(Declarative) UI 開發最本質的差異。在 JavaScript 的世界裡,這不僅僅是語法習慣的改變,而是一場深刻的「典範轉移(Paradigm Shift)」。

命令式編程:像食譜般的細節控

在 React 普及之前,我們大多數時間都在編寫「命令式」的程式碼。所謂命令式,核心在於 「如何做(How)」

當你使用原生 JavaScript(Vanilla JS)或 jQuery 操縱網頁時,你必須精確地告訴瀏覽器每一個動作。讓我們看一個最經典的例子:一個簡單的計數器。

命令式計數器的實作

假設我們有一個按鈕和一個顯示數字的 <span>

// 命令式開發風格
let count = 0;

const countDisplay = document.getElementById('count-display');
const incrementBtn = document.getElementById('increment-btn');

incrementBtn.addEventListener('click', () => {
// 1. 手動更新 JS 的資料狀態
count++;

// 2. 手動尋找 DOM 節點
// 3. 手動將資料同步到畫面上
countDisplay.textContent = count;

// 4. (選配) 如果我們想要數字變紅色當 count > 5
if (count > 5) {
countDisplay.style.color = 'red';
}
});

這段程式碼看起來非常直觀,對吧?但請注意這背後的隱憂。作為開發者,你承擔了「同步(Synchronization)」的全部責任。

命令式的痛苦:狀態分散與同步代價

當應用程式規模很小時,手動同步沒什麼大問題。但當功能變複雜時,問題就會接踵而至:

  1. 狀態不一致(State Inconsistency): 在上面的例子中,數值「事實」存在於兩個地方:JS 的 count 變數,以及 DOM 節點的 textContent。如果某段程式碼不小心更新了變數卻忘了更新 DOM,畫面就會顯示錯誤的資訊。這就是所謂的「UI 與狀態失步」。
  2. 脆弱的 DOM 依賴: 你的程式碼強烈依賴於 HTML 的結構(例如 id="count-display")。如果哪天設計師把 id 改了,或者結構變了,你的 JS 就會直接崩潰。
  3. 組合爆炸: 想像一下,如果這個計數器還會影響導航列的通知數量、存檔按鈕的啟用狀態、以及背景顏色。在命令式模式下,你必須在 click 事件中寫下一長串的「更新這、更新那」的指令。隨著 UI 元素增加,這些手動同步的連結會呈指數型增長。

宣告式編程:描述結果的藝術

React 帶領我們走向了「宣告式」的路徑。宣告式的核心在於 「要做什麼(What)」,而非如何做。

你不再需要告訴 React 如何去抓取 DOM 元素,也不需要告訴它何時該修改 textContent。你只需要描述:「在目前這個狀態下,UI 應該長什麼樣子。」

宣告式的核心公式

React 的設計哲學可以濃縮成一個簡單的數學公式:

UI = f(state)

  • state(狀態): 應用程式目前的數據(例如 count = 5)。
  • f(函數): 你的 React 元件。它像是一個純粹的轉換器。
  • UI(使用者介面): 最終產出的畫面。

只要 state 改變了,React 就會重新執行這個 f,計算出新的 UI。開發者只需要關注 state 的管理,剩下的「如何更新 DOM」都交給 React 處理。

宣告式計數器的實作

讓我們用 React 的方式重新改寫計數器:

import React, { useState } from 'react';

const Counter = () => {
const [count, setCount] = useState(0);

// 我們只定義「點擊時狀態如何改變」
// 我們完全不碰 DOM
const handleIncrement = () => {
setCount(count + 1);
};

// 我們「宣告」UI 的樣子,它會自動隨 count 同步
return (
<div>
<span style={{ color: count > 5 ? 'red' : 'black' }}>
{count}
</span>
<button onClick={handleIncrement}>累加</button>
</div>
);
};

在這裡,你看不到 getElementById,看不到 textContent 的賦值。你只是宣告了:「如果 count 大於 5,顏色就是紅色,否則就是黑色。」這就是宣告式的威力。

為什麼這是一場思維的「典範轉移」?

從命令式轉向宣告式,不只是換個 API 呼叫而已,它徹底改變了我們解決問題的方式。

1. 減少「副作用」帶來的 Bug

在命令式開發中,每一個 DOM 操作都是一個「副作用(Side Effect)」。你很難追蹤到底是哪一行程式碼把那個按鈕隱藏了。但在 React 中,如果按鈕隱藏了,你只需要檢查 state 是什麼,以及元件的 return 邏輯。這種 「資料驅動(Data-Driven)」 的模式讓除錯變得極其容易。

2. UI 狀態的「單一真理來源(Single Source of Truth)」

在宣告式架構中,狀態(State)是唯一的真理。UI 只是狀態的鏡像投影。你不再需要去查詢 DOM 來確認目前的數值是多少,你只需要看 state 就好。這消除了「狀態不一致」的可能性。

3. 開發效率的飛躍

當你不需要處理繁瑣的 DOM 更新邏輯時,你可以把心力放在更重要的商務邏輯上。

  • 命令式思維: 「當使用者輸入文字時,我要找到那個錯誤提示的 div,把它設為顯示,並把文字改成錯誤訊息...」
  • 宣告式思維: 「這個畫面的狀態包含 inputValuehasError。如果 hasError 為真,就顯示提示元件。」

預測與揭曉:React 是如何做到的?

你可能會好奇:「如果我不手動操作 DOM,React 怎麼知道哪裡要更新?它難道是每次都把整個網頁拆掉重蓋嗎?」

如果真的是每次重蓋,效能絕對會慘不忍睹。這就是為什麼 React 引入了 Virtual DOM(虛擬 DOM)。在下一節中,我們會深入探討 React 如何利用這種輕量級的 JS 物件來「描述」你宣告的結果,並以極高的效率將它同步到真實的網頁上。

深度對比:當應用程式變大時

為了讓你更深刻理解為什麼 React 堅持宣告式,我們來玩一個思維實驗。

假設你正在開發一個類似 Facebook 的導航列,它有以下需求:

  1. 顯示未讀訊息數量。
  2. 如果有新訊息,鈴鐺圖示要搖晃。
  3. 如果訊息超過 99 則,顯示 "99+"。
  4. 使用者點擊鈴鐺後,數量歸零,搖晃停止,並彈出選單。

命令式處理(jQuery 風格): 你會寫出一堆「如果...就...」的碎片。當點擊事件發生時,你要記得去改 innerText、移除 CSS class shake、顯示選單 div。如果你後來又加了一個「靜音模式」的功能,你可能又要回頭去改這幾個地方的邏輯,程式碼很快就會變成一團亂麻,也就是所謂的「義大利麵條程式碼(Spaghetti Code)」。

宣告式處理(React 風格): 你會定義一個狀態物件 { unreadCount: 10, isMuted: false, isMenuOpen: false }。 你的 UI 描述會是:

  • 鈴鐺是否搖晃?取決於 unreadCount > 0 && !isMuted
  • 文字顯示什麼?unreadCount > 99 ? '99+' : unreadCount
  • 選單是否顯示?取決於 isMenuOpen

無論需求如何增加,你的邏輯依然清晰地集中在「狀態與畫面的映射關係」上,而不是散落在各個事件處理函數中。

總結與銜接

命令式 UI 開發讓我們專注於過程,雖然精確,但在處理複雜邏輯時極易出錯且難以維護。宣告式 UI 開發則讓我們轉向專注於結果,透過 UI = f(state) 的公式,將繁瑣的 DOM 同步工作交給了 React。

這種思維的轉變是學習 React 最關鍵的一步。如果你能開始思考「我的狀態是什麼」而不是「我要怎麼改那個按鈕」,你才算真正踏入了 React 的大門。

核心要點總結

  • 命令式(How): 像食譜,手動操作 DOM,容易導致狀態與畫面不同步。
  • 宣告式(What): 像照片,描述 UI 在特定狀態下的模樣,由 React 負責同步。
  • UI = f(state): React 的核心公式,狀態是唯一的真理來源。

銜接下一個主題

既然我們已經學會了如何「描述」UI 的結果,那麼 React 實際上是用什麼來代表這些描述呢?直接操作真實的 DOM 物件是非常昂貴且緩慢的。在下一節,我們將解開 Virtual DOM 的神祕面紗,看看 React 如何用輕量級的 JavaScript 物件來代表我們宣告的 UI,以及 JSX 這種奇特的語法背後真正的樣貌。

關鍵思維:這不是唯一的選擇,但它是現代 UI 的首選

雖然我們大力推崇宣告式,但請記住:這是一個權衡(Trade-off)。宣告式需要一個強大的框架(如 React)來幫你處理背後的轉換邏輯。在某些極端情況下(例如開發高效能的遊戲引擎或直接操作 Canvas 繪圖),命令式依然有其不可替代的精確性。

但在網頁應用開發中,宣告式帶來的可預測性與開發效率,已經讓它成為了無可爭議的標準。理解了這一點,你就能明白為什麼 React 要設計成現在這個樣子。

重點回顧與聯繫

在進入下一個部分前,請確保你掌握了以下核心概念:

概念歸納

  1. 宣告式思維:開發者只需定義「資料」與「畫面」的對應關係。
  2. 單一事實來源:狀態 (State) 決定了一切,不需要再去 DOM 裡面翻找資料。
  3. 抽象化 DOM:React 將底層繁瑣的操作隱藏起來,讓我們能以更高的抽象層級思考問題。

連結下一章:Virtual DOM

宣告式開發讓開發者「宣告」了想要的 UI,但這些宣告在底層是如何被「記錄」下來的? React 並不是直接操作 DOM,而是先在記憶體中建立一個輕量級的副本。

預告問題: 當你寫下 <div>Hello</div> 時,這真的是 HTML 嗎? 其實,它在 JavaScript 的世界裡是一個特定的物件格式。下一節我們將拆解這個物件的結構,看看 Virtual DOM 的真實面目。