Redux 基礎,第 2 部分:概念和資料流程
Redux 基礎,第 2 部分:概念和資料流程
- 使用 Redux 的關鍵術語和概念
- 資料如何流經 Redux 應用程式
簡介
在 第 1 部分:Redux 概觀 中,我們討論了 Redux 是什麼、為什麼您可能想要使用它,並列出了通常與 Redux 核心一起使用的其他 Redux 函式庫。我們還看到了工作中 Redux 應用程式的簡要範例,以及組成應用程式的部分。最後,我們簡要提到了 Redux 中使用的一些術語和概念。
在本節中,我們將更詳細地探討這些術語和概念,並進一步討論資料如何流經 Redux 應用程式。
背景概念
在我們深入探討實際程式碼之前,讓我們來討論一些您需要知道才能使用 Redux 的術語和概念。
狀態管理
讓我們從查看一個小型 React 計數器元件開始。它會追蹤元件狀態中的數字,並在按一下按鈕時遞增數字
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)
// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}
它是一個包含以下部分的獨立應用程式
- 狀態,驅動我們應用程式的真實來源;
- 檢視,基於目前狀態的 UI 宣告式描述
- 動作,基於使用者輸入而發生在應用程式中的事件,並觸發狀態更新
這是「單向資料流」的一個小範例
- 狀態描述應用程式在特定時間點的狀態
- UI 會根據該狀態進行渲染
- 當發生某些事情(例如使用者按一下按鈕)時,狀態會根據所發生的事件進行更新
- UI 會根據新的狀態重新渲染
然而,當我們有多個元件需要共用和使用相同的狀態時,這種簡潔性可能會崩潰,特別是如果這些元件位於應用程式的不同部分時。有時這可以用「提升狀態」到父元件來解決,但這並非總是有用。
解決此問題的方法之一是從元件中提取共用狀態,並將其放入元件樹外部的集中位置。這樣一來,我們的元件樹就會變成一個大型「檢視」,而且任何元件都可以存取狀態或觸發動作,無論它們在樹中的哪個位置!
透過定義和區分狀態管理中涉及的概念,並強制執行維護檢視和狀態之間獨立性的規則,我們可以讓我們的程式碼更具結構和可維護性。
這是 Redux 背後的基本概念:一個單一的集中式位置,用來包含應用程式中的全域狀態,以及在更新該狀態時遵循特定模式,以使程式碼可預測。
不可變性
「可變」表示「可變更」。如果某個東西是「不可變」的,就永遠無法變更。
JavaScript 物件和陣列預設都是可變的。如果我建立一個物件,我可以變更其欄位的內容。如果我建立一個陣列,我也可以變更其內容
const obj = { a: 1, b: 2 }
// still the same object outside, but the contents have changed
obj.b = 3
const arr = ['a', 'b']
// In the same way, we can change the contents of this array
arr.push('c')
arr[1] = 'd'
這稱為變異物件或陣列。這是記憶體中相同的物件或陣列參考,但現在物件內的內容已變更。
為了不可變地更新值,程式碼必須製作現有物件/陣列的副本,然後修改副本.
我們可以使用 JavaScript 的陣列/物件擴散運算子來手動執行此操作,以及會傳回陣列新副本(而不是變異原始陣列)的陣列方法
const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}
const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}
const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')
// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')
Redux 預期所有狀態更新都是不可變地完成的。稍後我們將探討這一點的重要性以及這樣做的時機,以及撰寫不可變更新邏輯的一些更簡單的方法。
如需有關不可變性如何在 JavaScript 中運作的更多資訊,請參閱
Redux 術語
在繼續之前,您需要熟悉一些重要的 Redux 術語
動作
動作是一個具有 type
欄位的純 JavaScript 物件。您可以將動作視為描述應用程式中發生某件事的事件。
type
欄位應為一個字串,提供此動作一個描述性名稱,例如 "todos/todoAdded"
。我們通常會像 "domain/eventName"
這樣撰寫類型字串,其中第一部分是此動作所屬的功能或類別,第二部分是發生的特定事件。
動作物件可以有其他欄位,其中包含有關發生事件的其他資訊。根據慣例,我們將該資訊放入名為 payload
的欄位中。
典型的動作物件可能如下所示
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
簡化器
Reducer 是一個函式,它接收目前的 state
和一個 action
物件,決定是否需要更新 state,並回傳新的 state:(state, action) => newState
。你可以將 reducer 視為一個事件監聽器,它根據接收到的 action(事件)類型來處理事件。
"Reducer" 函式之所以這麼命名,是因為它們類似於你傳遞給 Array.reduce()
方法的回呼函式類型。
Reducer 必須始終遵循一些特定規則
- 它們應該僅根據
state
和action
參數計算新的 state 值 - 它們不能修改現有的
state
。相反,它們必須進行不可變更新,方法是複製現有的state
並對複製的值進行更改。 - 它們不能執行任何非同步邏輯、計算隨機值或導致其他「副作用」
我們稍後將詳細討論 reducer 的規則,包括它們為什麼重要以及如何正確遵循它們。
Reducer 函式內的邏輯通常遵循相同的步驟
- 檢查 reducer 是否關心這個動作
- 如果是,複製 state,使用新值更新副本,並回傳它
- 否則,回傳現有的 state,不變更
以下是一個 reducer 的小範例,展示每個 reducer 應遵循的步驟
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/incremented') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}
Reducer 可以使用任何類型的邏輯來決定新的 state 應該是:if/else
、switch
、迴圈等。
詳細說明:為什麼它們被稱為「Reducer」?
Array.reduce()
方法讓你取得一個值陣列,一次處理陣列中的每個項目,並回傳一個最終結果。你可以將它視為「將陣列簡化為一個值」。
Array.reduce()
將回呼函式作為引數,會對陣列中的每個項目呼叫一次。它有兩個引數
previousResult
,這是您的回呼函式上次傳回的值currentItem
,這是陣列中的目前項目
回呼函式第一次執行時,沒有可用的 previousResult
,因此我們也需要傳入一個初始值,它將用作第一個 previousResult
。
如果我們想要將陣列中的數字加總以找出總和,我們可以撰寫一個看起來像這樣的 reduce 回呼函式
const numbers = [2, 5, 8]
const addNumbers = (previousResult, currentItem) => {
console.log({ previousResult, currentItem })
return previousResult + currentItem
}
const initialValue = 0
const total = numbers.reduce(addNumbers, initialValue)
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}
console.log(total)
// 15
請注意,這個 addNumbers
「reduce 回呼函式」不需要自己追蹤任何內容。它會取得 previousResult
和 currentItem
引數,對它們執行一些動作,並傳回新的結果值。
Redux reducer 函式與這個「reduce 回呼函式」的構想完全相同! 它會取得「先前的結果」(state
)和「目前的項目」(action
物件),根據這些引數決定新的 state 值,並傳回那個新的 state。
如果我們要建立一個 Redux 動作陣列,呼叫 reduce()
,並傳入一個 reducer 函式,我們將以相同的方式取得最終結果
const actions = [
{ type: 'counter/incremented' },
{ type: 'counter/incremented' },
{ type: 'counter/incremented' }
]
const initialState = { value: 0 }
const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}
我們可以說 Redux reducer 會將一組動作(隨著時間推移)簡化成單一 state。不同之處在於,使用 Array.reduce()
時,它會一次發生,而使用 Redux 時,它會在執行中應用程式的生命週期中發生。
Store
目前的 Redux 應用程式 state 存在於稱為 store 的物件中。
透過傳入 reducer 來建立 store,它有一個稱為 getState
的方法,會傳回目前的 state 值
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
Dispatch
Redux store 有個稱為 dispatch
的方法。更新 state 的唯一方法是呼叫 store.dispatch()
並傳入一個 action 物件。store 會執行它的 reducer 函式,並將新的 state 值儲存在內部,我們可以呼叫 getState()
來擷取更新的值
store.dispatch({ type: 'counter/incremented' })
console.log(store.getState())
// {value: 1}
您可以將 dispatching 動作視為在應用程式中「觸發事件」。發生了一些事,我們希望 store 知道。reducer 會像事件監聽器一樣運作,當它們聽到有興趣的動作時,它們會更新 state 以作為回應。
選取器
選取器是函式,知道如何從儲存狀態值中擷取特定資訊片段。隨著應用程式變大,這有助於避免重複邏輯,因為應用程式的不同部分需要讀取相同的資料
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
核心概念和原則
總體而言,我們可以將 Redux 設計背後的意圖總結為三個核心概念
單一真實來源
應用程式的全域狀態儲存在單一儲存內的物件中。任何特定資料片段都應該只存在於一個位置,而不是在許多地方重複。
這使得在變更時更容易除錯和檢查應用程式的狀態,以及集中需要與整個應用程式互動的邏輯。
這不表示應用程式中的每個狀態片段都必須放入 Redux 儲存!你應該根據需要的位置來決定狀態片段屬於 Redux 或你的 UI 元件。
狀態為唯讀
變更狀態的唯一方法是發送動作,一個描述發生什麼事的物件。
這樣,UI 就不會意外覆寫資料,而且更容易追蹤狀態更新發生的原因。由於動作是純粹的 JS 物件,因此可以記錄、序列化、儲存,並稍後重新播放以進行除錯或測試。
變更使用純粹的簡化器函式進行
若要根據動作指定如何更新狀態樹,請撰寫簡化器函式。簡化器是純粹的函式,它會取得前一個狀態和一個動作,並傳回下一個狀態。就像任何其他函式一樣,你可以將簡化器拆分為較小的函式來協助執行工作,或為常見任務撰寫可重複使用的簡化器。
Redux 應用程式資料流程
稍早,我們討論了「單向資料流程」,它描述了更新應用程式的步驟順序
- 狀態描述應用程式在特定時間點的狀態
- UI 會根據該狀態進行渲染
- 當發生某些事情(例如使用者按一下按鈕)時,狀態會根據所發生的事件進行更新
- UI 會根據新的狀態重新渲染
針對 Redux 來說,我們可以更詳細地說明這些步驟
- 初始設定
- 使用根部 reducer 函式建立 Redux 儲存區
- 儲存區呼叫根部 reducer 一次,並將回傳值儲存為其初始的
狀態
- 當 UI 第一次呈現時,UI 元件會存取 Redux 儲存區的目前狀態,並使用該資料來決定要呈現什麼。它們也會訂閱任何未來的儲存區更新,以便得知狀態是否已變更。
- 更新
- 應用程式中發生一些事情,例如使用者按一下按鈕
- 應用程式程式碼會將動作傳送給 Redux 儲存區,例如
dispatch({type: 'counter/incremented'})
- 儲存區會再次執行 reducer 函式,並使用先前的
狀態
和目前的動作
,並將回傳值儲存為新的狀態
- 儲存區會通知所有已訂閱的 UI 部分,表示儲存區已更新
- 每個需要從儲存區取得資料的 UI 元件會檢查它們所需的狀態部分是否已變更。
- 每個發現其資料已變更的元件會強制使用新資料重新呈現,以便更新畫面中顯示的內容
以下是資料流程的視覺化表示
您已學到的內容
- Redux 的意圖可以用三個原則來總結
- 全域應用程式狀態儲存在單一儲存區中
- 儲存區狀態對應用程式的其他部分是唯讀的
- reducer 函式用於根據動作更新狀態
- Redux 使用「單向資料流程」應用程式結構
- 狀態描述應用程式在某個時間點的狀態,而 UI 則根據該狀態呈現
- 當應用程式中發生一些事情時
- UI 會傳送一個動作
- 儲存區會執行 reducer,而狀態會根據發生的事情更新
- 儲存區會通知 UI,表示狀態已變更
- UI 會根據新的狀態重新渲染
接下來是什麼?
現在您應該熟悉描述 Redux 應用程式不同部分的主要概念和術語。
現在,讓我們在第 3 部分:狀態、動作和簡化器中開始建立新的 Redux 應用程式時,看看這些部分是如何協同運作的。