Skip to main content

單向資料流與一律重繪渲染策略

單向資料流

什麼是單向資料流?

單向資料流(Unidirectional Data Flow)是一種前端開發的設計模式,特別在 React 中被廣泛使用。它的核心概念是資料的流動方向是單一的,從父元件流向子元件,確保資料的變更有明確的來源與路徑。這樣可以讓應用程式的狀態(state)管理更可預測、易於維護。

在單向資料流中:

  • 資料(state) 通常儲存在頂層元件(或狀態管理工具,如 Redux)。

  • 父元件透過 props 將資料傳遞給子元件。

  • 子元件無法直接修改父元件的資料,只能透過父元件提供的回呼函數(callback)來通知父元件進行狀態更新。

  • 狀態更新後,React 會重新渲染相關元件,反映最新的資料。

為什麼需要單向資料流?

  1. 可預測性:資料流動路徑清晰,容易追蹤資料變更的來源。

  2. 易於除錯:當資料有問題時,可以快速找到負責管理的元件。

  3. 可維護性:隨著專案規模擴大,單向資料流讓程式碼結構更整潔。

單向資料流的運作流程

  1. 狀態初始化:在父元件中定義狀態(state)。

  2. 傳遞資料:父元件透過 props 將狀態傳給子元件。

  3. 觸發更新:子元件透過回呼函數通知父元件更新狀態。

  4. 重新渲染:父元件狀態改變後,React 重新渲染相關元件,更新畫面。


實現單向資料流的 DOM 渲染策略

在 React 中,實現單向資料流的 DOM 渲染策略依賴於 元件層次結構狀態管理。以下是一個完整的範例,展示如何用 React 實現單向資料流,並確保 DOM 根據資料變化正確渲染。

範例:計數器應用程式

這個範例展示一個簡單的計數器,父元件管理計數狀態,子元件負責顯示計數並提供按鈕觸發更新。

程式碼實現

// App.jsx (父元件)
import React, { useState } from 'react';
import CounterDisplay from './CounterDisplay';

function App() {
// 定義狀態
const [count, setCount] = useState(0);

// 定義更新狀態的回呼函數
const increment = () => {
setCount(count + 1);
};

const decrement = () => {
setCount(count - 1);
};

return (
<div>
<h1>計數器應用程式</h1>
{/* 透過 props 傳遞資料與回呼函數給子元件 */}
<CounterDisplay count={count} onIncrement={increment} onDecrement={decrement} />
</div>
);
}

export default App;

// CounterDisplay.jsx (子元件)
import React from 'react';

function CounterDisplay({ count, onIncrement, onDecrement }) {
return (
<div>
<h2>目前計數:{count}</h2>
<button onClick={onIncrement}>增加</button>
<button onClick={onDecrement}>減少</button>
</div>
);
}

export default CounterDisplay;

程式碼逐步解析

  1. 父元件(App.jsx)

    • 使用 useState 鉤子定義 count 狀態,初始值為 0。

    • 定義兩個回呼函數 increment 和 decrement,用來更新 count 狀態。

    • 透過 props 將 count 和回呼函數傳遞給子元件 CounterDisplay。

  2. 子元件(CounterDisplay.jsx)

    • 接收父元件傳來的 count(顯示計數)以及 onIncrement 和 onDecrement(回呼函數)。

    • 子元件只負責顯示資料和觸發事件,不直接修改狀態。

    • 當使用者點擊「增加」或「減少」按鈕時,呼叫父元件的回呼函數,通知父元件更新狀態。

  3. 單向資料流的實現

    • 資料從父元件(count)流向子元件(透過 props)。

    • 子元件無法直接修改 count,只能透過父元件的回呼函數間接觸發更新。

    • 當 count 改變時,React 自動重新渲染 App 和 CounterDisplay,更新畫面。

運行結果

  • 畫面顯示當前計數(例如「目前計數:0」)。

  • 點擊「增加」按鈕,計數加 1;點擊「減少」按鈕,計數減 1。

  • 所有狀態變更都由父元件控制,子元件只負責顯示和觸發事件。


React 中的一律重繪渲染策略

什麼是一律重繪渲染策略?

React 的渲染策略是一律重繪(Re-render Everything),這意味著當元件的狀態(state)或屬性(props)發生變化時,React 會重新執行該元件的渲染函數,生成新的虛擬 DOM(Virtual DOM),然後與舊的虛擬 DOM 比較(diffing),最終只更新實際 DOM 中有變化的部分。

這種策略的特點:

  • 簡單直接:React 不假設哪些部分需要更新,而是假設所有元件都可能需要重新渲染。

  • 高效更新:透過虛擬 DOM 和 diffing 算法,React 確保只更新必要的 DOM 節點,減少實際 DOM 操作的開銷。

  • 依賴狀態和 props:元件的渲染結果完全由其 state 和 props 決定,確保一致的畫面輸出。

一律重繪的工作流程

  1. 狀態或 props 改變:元件的 state 或 props 更新,觸發重新渲染。

  2. 生成新虛擬 DOM:React 執行元件的渲染函數,生成新的虛擬 DOM 樹。

  3. 比較虛擬 DOM(diffing):React 比較新舊虛擬 DOM,找出差異。

  4. 更新真實 DOM:只更新有變化的部分,保持高效能。

範例:動態列表渲染

以下展示一個動態列表,當使用者輸入新項目時,列表會自動更新,體現一律重繪的策略。

程式碼實現

// App.jsx
import React, { useState } from 'react';
import TodoList from './TodoList';

function App() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');

const addTodo = () => {
if (inputValue.trim() !== '') {
setTodos([...todos, inputValue]);
setInputValue('');
}
};

return (
<div>
<h1>待辦事項清單</h1>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="輸入待辦事項"
/>
<button onClick={addTodo}>新增</button>
<TodoList todos={todos} />
</div>
);
}

export default App;

// TodoList.jsx
import React from 'react';

function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
);
}

export default TodoList;

程式碼逐步解析

  1. 父元件(App.jsx)

    • 使用 useState 管理 todos(待辦事項陣列)和 inputValue(輸入框內容)。

    • 當使用者輸入文字並點擊「新增」按鈕時,呼叫 addTodo 函數,將新項目加入 todos 並清空輸入框。

    • 透過 props 將 todos 傳遞給子元件 TodoList。

  2. 子元件(TodoList.jsx)

    • 接收 todos props,並使用 map 渲染待辦事項清單。

    • 每當 todos 改變時,React 會重新執行 TodoList 的渲染函數,生成新的虛擬 DOM。

  3. 一律重繪的實現

    • 當 todos 陣列更新時(例如新增項目),React 重新渲染 App 和 TodoList 元件。

    • React 比較新舊虛擬 DOM,發現只有 <ul> 的內容有變化,因此只更新 <ul> 部分的真實 DOM。

    • 雖然 React 執行了「一律重繪」,但透過虛擬 DOM 的 diffing,實際 DOM 更新是高效的。

運行結果

  • 使用者輸入待辦事項並點擊「新增」,清單會動態更新。

  • 每次狀態改變,React 重新渲染相關元件,但只更新必要的 DOM 節點(例如新增的 <li>)。

優化一律重繪的效能,使用 React.memo 和 useCallback

雖然 React 的一律重繪策略簡單高效,但在複雜應用中,可能需要優化以減少不必要的渲染。

複雜範例:帶有刪除功能的待辦事項清單

假設 TodoList 的子元件 TodoItem 負責顯示單個待辦事項,並包含刪除按鈕。TodoItem 的渲染成本較高(例如包含複雜樣式或子元件),我們需要確保它不會不必要地重新渲染。

// App.jsx
import React, { useState, useCallback } from 'react';
import TodoList from './TodoList';

function App() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');

const addTodo = useCallback(() => {
if (inputValue.trim() !== '') {
setTodos([...todos, inputValue]);
setInputValue('');
}
}, [todos, inputValue]);

const deleteTodo = useCallback((index) => {
setTodos(todos.filter((_, i) => i !== index));
}, [todos]);

return (
<div>
<h1>待辦事項清單</h1>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="輸入待辦事項"
/>
<button onClick={addTodo}>新增</button>
<TodoList todos={todos} deleteTodo={deleteTodo} />
</div>
);
}

export default App;

// TodoList.jsx
import React from 'react';
import TodoItem from './TodoItem';

function TodoList({ todos, deleteTodo }) {
return (
<ul>
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} index={index} deleteTodo={deleteTodo} />
))}
</ul>
);
}

export default TodoList;

// TodoItem.jsx
import React from 'react';

const TodoItem = React.memo(({ todo, index, deleteTodo }) => {
console.log(`Rendering TodoItem ${index}`); // 用於觀察渲染次數
return (
<li>
{todo}
<button onClick={() => deleteTodo(index)}>刪除</button>
</li>
);
});

export default TodoItem;

為什麼在這個範例中使用 React.memo 和 useCallback?

  1. 使用 React.memo 的理由

    • TodoItem 是獨立的子元件,假設它包含複雜的渲染邏輯(例如樣式、子元件或計算)。

    • 當父元件 App 因 inputValue 改變而重新渲染時,todos 和 deleteTodo 可能不變。使用 React.memo 確保 TodoItem 不會因父元件渲染而無故重新渲染。

    • 透過 console.log,你可以觀察到 TodoItem 只在 todo 或 index 改變時渲染。

  2. 使用 useCallback 的理由

    • deleteTodo 函數傳遞給 TodoItem,而 TodoItem 使用了 React.memo。

    • 如果不使用 useCallback,每次 App 渲染時,deleteTodo 會是一個新函數參考,導致 React.memo 的淺比較失敗,TodoItem 會不必要地重新渲染。

    • 使用 useCallback 確保 deleteTodo 的參考在 todos 不變時穩定,減少 TodoItem 的渲染次數。

運行結果

  • 當輸入文字時,inputValue 改變,App 重新渲染,但 TodoItem 不會重新渲染(因為 todos 和 deleteTodo 未變)。

  • 當新增或刪除待辦事項時,todos 改變,TodoList 和相關的 TodoItem 會重新渲染,更新畫面。


總結

  1. 單向資料流

    • 資料從父元件流向子元件,子元件透過回呼函數通知父元件更新狀態。

    • 範例中,計數器和待辦事項清單展示了資料如何從父元件傳遞到子元件,並透過回呼函數更新。

  2. 實現單向資料流的 DOM 渲染

    • React 透過 props 和 state 管理資料流動,確保 DOM 根據狀態變化正確更新。

    • 計數器範例展示了父子元件間的資料傳遞與事件處理。

  3. 一律重繪渲染策略

    • React 在狀態或 props 改變時重新渲染元件,生成新虛擬 DOM,並透過 diffing 算法高效更新真實 DOM。

    • 待辦事項清單範例展示了動態列表如何根據狀態變化重新渲染。

  4. 什麼時候使用 React.memo 和 useCallback

    • 使用 React.memo:

      • 當元件的渲染成本高(例如複雜的 DOM 結構或計算)。

      • 當 props 穩定,且父元件頻繁重新渲染。

    • 使用 useCallback:

      • 當回呼函數傳遞給 React.memo 包裝的子元件。

      • 當依賴項相對穩定,記憶化能有效減少函數參考的改變。

  5. 最佳實踐

    • 在小型應用或簡單元件中,優先保持程式碼簡潔,避免過早優化。

    • 在複雜應用中,針對高成本元件或頻繁渲染的場景,使用 React.memo 和 useCallback 優化效能。

    • 使用工具(如 React Developer Tools)檢查元件渲染次數,確認優化是否有效。