跳至主要內容

Redux 基礎,第 2 部分:概念和資料流程

Redux 基礎,第 2 部分:概念和資料流程

您將會學到
  • 使用 Redux 的關鍵術語和概念
  • 資料如何流經 Redux 應用程式

簡介

第 1 部分:Redux 概觀 中,我們討論了 Redux 是什麼、為什麼您可能想要使用它,並列出了通常與 Redux 核心一起使用的其他 Redux 函式庫。我們還看到了工作中 Redux 應用程式的簡要範例,以及組成應用程式的部分。最後,我們簡要提到了 Redux 中使用的一些術語和概念。

在本節中,我們將更詳細地探討這些術語和概念,並進一步討論資料如何流經 Redux 應用程式。

請注意,本教學課程故意顯示較舊的 Redux 邏輯模式,這些模式需要比我們當前教授的 Redux Toolkit「現代 Redux」模式更多的程式碼,以說明 Redux 背後的原則和概念。這並非預設為可供生產環境使用的專案。

請參閱這些頁面,以瞭解如何使用 Redux Toolkit 的「現代 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 會根據新的狀態重新渲染

One-way data flow

然而,當我們有多個元件需要共用和使用相同的狀態時,這種簡潔性可能會崩潰,特別是如果這些元件位於應用程式的不同部分時。有時這可以用「提升狀態」到父元件來解決,但這並非總是有用。

解決此問題的方法之一是從元件中提取共用狀態,並將其放入元件樹外部的集中位置。這樣一來,我們的元件樹就會變成一個大型「檢視」,而且任何元件都可以存取狀態或觸發動作,無論它們在樹中的哪個位置!

透過定義和區分狀態管理中涉及的概念,並強制執行維護檢視和狀態之間獨立性的規則,我們可以讓我們的程式碼更具結構和可維護性。

這是 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 必須始終遵循一些特定規則

  • 它們應該僅根據 stateaction 參數計算新的 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/elseswitch、迴圈等。

詳細說明:為什麼它們被稱為「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 回呼函式」不需要自己追蹤任何內容。它會取得 previousResultcurrentItem 引數,對它們執行一些動作,並傳回新的結果值。

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 data flow diagram

您已學到的內容

摘要
  • Redux 的意圖可以用三個原則來總結
    • 全域應用程式狀態儲存在單一儲存區中
    • 儲存區狀態對應用程式的其他部分是唯讀的
    • reducer 函式用於根據動作更新狀態
  • Redux 使用「單向資料流程」應用程式結構
    • 狀態描述應用程式在某個時間點的狀態,而 UI 則根據該狀態呈現
    • 當應用程式中發生一些事情時
      • UI 會傳送一個動作
      • 儲存區會執行 reducer,而狀態會根據發生的事情更新
      • 儲存區會通知 UI,表示狀態已變更
    • UI 會根據新的狀態重新渲染

接下來是什麼?

現在您應該熟悉描述 Redux 應用程式不同部分的主要概念和術語。

現在,讓我們在第 3 部分:狀態、動作和簡化器中開始建立新的 Redux 應用程式時,看看這些部分是如何協同運作的。