單向資料流與一律重繪渲染策略
單向資料流
什麼是單向資料流?
單向資料流(Unidirectional Data Flow)是一種前端開發的設計模式,特別在 React 中被廣泛使用。它的核心概念是資料的流動方向是單一的,從父元件流向子元件,確保資料的變更有明確的來源與路徑。這樣可以讓應用程式的狀態(state)管理更可預測、易於維護。
在單向資料流中:
-
資料(state) 通常儲存在頂層元件(或狀態管理工具,如 Redux)。
-
父元件透過 props 將資料傳遞給子元件。
-
子元件無法直接修改父元件的資料,只能透過父元件提供的回呼函數(callback)來通知父元件進行狀態更新。
-
狀態更新後,React 會重新渲染相關元件,反映最新的資料。
為什麼需要單向資料流?
-
可預測性:資料流動路徑清晰,容易追蹤資料變更的來源。
-
易於除錯:當資料有問題時,可以快速找到負責管理的元件。
-
可維護性:隨著專案規模擴大,單向資料流讓程式碼結構更整潔。
單向資料流的運作流程
-
狀態初始化:在父元件中定義狀態(state)。
-
傳遞資料:父元件透過 props 將狀態傳給子元件。
-
觸發更新:子元件透過回呼函數通知父元件更新狀態。
-
重新渲染:父元件狀態改變後,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;
程式碼逐步解析
-
父元件(App.jsx):
-
使用 useState 鉤子定義 count 狀態,初始值為 0。
-
定義兩個回呼函數 increment 和 decrement,用來更新 count 狀態。
-
透過 props 將 count 和回呼函數傳遞給子元件 CounterDisplay。
-
-
子元件(CounterDisplay.jsx):
-
接收父元件傳來的 count(顯示計數)以及 onIncrement 和 onDecrement(回呼函數)。
-
子元件只負責顯示資料和觸發事件,不直接修改狀態。
-
當使用者點擊「增加」或「減少」按鈕時,呼叫父元件的回呼函數,通知父元件更新狀態。
-
-
單向資料流的實現:
-
資料從父元件(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 決定,確保一致的畫面輸出。
一律重繪的工作流程
-
狀態或 props 改變:元件的 state 或 props 更新,觸發重新渲染。
-
生成新虛擬 DOM:React 執行元件的渲染函數,生成新的虛擬 DOM 樹。
-
比較虛擬 DOM(diffing):React 比較新舊虛擬 DOM,找出差異。
-
更新真實 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;
程式碼逐步解析
-
父元件(App.jsx):
-
使用 useState 管理 todos(待辦事項陣列)和 inputValue(輸入框內容)。
-
當使用者輸入文字並點擊「新增」按鈕時,呼叫 addTodo 函數,將新項目加入 todos 並清空輸入框。
-
透過 props 將 todos 傳遞給子元件 TodoList。
-
-
子元件(TodoList.jsx):
-
接收 todos props,並使用 map 渲染待辦事項清單。
-
每當 todos 改變時,React 會重新執行 TodoList 的渲染函數,生成新的虛擬 DOM。
-
-
一律重繪的實現:
-
當 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?
-
使用 React.memo 的理由:
-
TodoItem 是獨立的子元件,假設它包含複雜的渲染邏輯(例如樣式、子元件或計算)。
-
當父元件 App 因 inputValue 改變而重新渲染時,todos 和 deleteTodo 可能不變。使用 React.memo 確保 TodoItem 不會因父元件渲染而無故重新渲染。
-
透過 console.log,你可以觀察到 TodoItem 只在 todo 或 index 改變時渲染。
-
-
使用 useCallback 的理由:
-
deleteTodo 函數傳遞給 TodoItem,而 TodoItem 使用了 React.memo。
-
如果不使用 useCallback,每次 App 渲染時,deleteTodo 會是一個新函數參考,導致 React.memo 的淺比較失敗,TodoItem 會不必要地重新渲染。
-
使用 useCallback 確保 deleteTodo 的參考在 todos 不變時穩定,減少 TodoItem 的渲染次數。
-
運行結果
-
當輸入文字時,inputValue 改變,App 重新渲染,但 TodoItem 不會重新渲染(因為 todos 和 deleteTodo 未變)。
-
當新增或刪除待辦事項時,todos 改變,TodoList 和相關的 TodoItem 會重新渲染,更新畫面。
總結
-
單向資料流:
-
資料從父元件流向子元件,子元件透過回呼函數通知父元件更新狀態。
-
範例中,計數器和待辦事項清單展示了資料如何從父元件傳遞到子元件,並透過回呼函數更新。
-
-
實現單向資料流的 DOM 渲染:
-
React 透過 props 和 state 管理資料流動,確保 DOM 根據狀態變化正確更新。
-
計數器範例展示了父子元件間的資料傳遞與事件處理。
-
-
一律重繪渲染策略:
-
React 在狀態或 props 改變時重新渲染元件,生成新虛擬 DOM,並透過 diffing 算法高效更新真實 DOM。
-
待辦事項清單範例展示了動態列表如何根據狀態變化重新渲染。
-
-
什麼時候使用 React.memo 和 useCallback:
-
使用 React.memo:
-
當元件的渲染成本高(例如複雜的 DOM 結構或計算)。
-
當 props 穩定,且父元件頻繁重新渲染。
-
-
使用 useCallback:
-
當回呼函數傳遞給 React.memo 包裝的子元件。
-
當依賴項相對穩定,記憶化能有效減少函數參考的改變。
-
-
-
最佳實踐:
-
在小型應用或簡單元件中,優先保持程式碼簡潔,避免過早優化。
-
在複雜應用中,針對高成本元件或頻繁渲染的場景,使用 React.memo 和 useCallback 優化效能。
-
使用工具(如 React Developer Tools)檢查元件渲染次數,確認優化是否有效。
-