跳至主要内容

Redux 基础知识,第二部分:概念和数据流

Redux 基础知识,第二部分:概念和数据流

您将学到什么
  • 使用 Redux 的关键术语和概念
  • 数据如何在 Redux 应用中流动

介绍

第一部分:Redux 概述 中,我们讨论了 Redux 是什么,为什么您可能想要使用它,并列出了通常与 Redux 核心一起使用的其他 Redux 库。我们还看到了一个工作 Redux 应用的小例子,以及构成应用的各个部分。最后,我们简要提到了 Redux 中使用的一些术语和概念。

在本节中,我们将更详细地了解这些术语和概念,并更多地讨论数据如何在 Redux 应用中流动。

注意

请注意,本教程有意展示旧式的 Redux 逻辑模式,这些模式需要比我们今天作为构建 Redux 应用程序的正确方法所教授的“现代 Redux”模式(使用 Redux Toolkit)需要更多的代码,以便解释 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 术语。

操作

一个操作是一个简单的 JavaScript 对象,它有一个type字段。您可以将操作视为描述应用程序中发生的事情的事件

type 字段应该是一个字符串,用于为该操作提供描述性名称,例如 "todos/todoAdded"。我们通常将该类型字符串写成 "domain/eventName" 的形式,其中第一部分是该操作所属的功能或类别,第二部分是发生的具体事件。

操作对象可以包含其他字段,其中包含有关发生事件的附加信息。按照惯例,我们将这些信息放在名为 payload 的字段中。

一个典型的操作对象可能如下所示

const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}

Reducers

Reducer 是一个函数,它接收当前的 state 和一个 action 对象,决定是否需要更新状态,并返回新的状态:(state, action) => newState您可以将 Reducer 视为一个事件监听器,它根据接收到的操作(事件)类型处理事件。

info

"Reducer" 函数之所以得名,是因为它们类似于您传递给 Array.reduce() 方法的回调函数类型。

Reducer 必须始终遵循一些特定规则

  • 它们应该只根据 stateaction 参数计算新的状态值
  • 它们不允许修改现有的 state。相反,它们必须进行不可变更新,方法是复制现有的 state 并对复制的值进行更改。
  • 它们不得进行任何异步逻辑、计算随机值或导致其他“副作用”

我们将在后面详细讨论 Reducer 的规则,包括它们为何重要以及如何正确遵循这些规则。

Reducer 函数内部的逻辑通常遵循相同的步骤序列

  • 检查 Reducer 是否关心此操作
    • 如果是,则复制状态,使用新值更新副本,然后返回它
  • 否则,返回现有状态不变。

以下是一个简单的 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 可以使用任何类型的逻辑来决定新的状态应该是什么:if/elseswitch、循环等等。

详细解释:为什么它们被称为“Reducer”?

The 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 对象),根据这些参数决定一个新的状态值,并返回该新状态。

如果我们要创建一个 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 将一组操作(随着时间的推移)减少为一个单一状态。区别在于,使用 Array.reduce() 它是一次性完成的,而使用 Redux,它是在你的运行应用程序的整个生命周期中完成的。

Store

当前 Redux 应用程序状态存储在一个名为 store 的对象中。

store 是通过传入一个 reducer 创建的,并且有一个名为 getState 的方法,该方法返回当前状态值

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

分发

Redux 存储有一个名为 dispatch 的方法。**更新状态的唯一方法是调用 store.dispatch() 并传入一个 action 对象**。存储将运行其 reducer 函数并在其中保存新的状态值,我们可以调用 getState() 来检索更新后的值

store.dispatch({ type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}

**您可以将分发 action 视为在应用程序中“触发事件”**。发生了某些事情,我们希望存储知道它。Reducers 就像事件监听器,当它们听到感兴趣的 action 时,它们会相应地更新状态。

选择器

**选择器** 是知道如何从存储状态值中提取特定信息片段的函数。随着应用程序的增长,这可以帮助避免重复逻辑,因为应用程序的不同部分需要读取相同的数据

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

核心概念和原则

总的来说,我们可以将 Redux 设计背后的意图概括为三个核心概念

单一事实来源

应用程序的**全局状态**存储为单个**存储**中的一个对象。任何给定的数据片段都应该只存在于一个位置,而不是在许多地方重复。

这使得在事物发生变化时更容易调试和检查应用程序的状态,以及集中需要与整个应用程序交互的逻辑。

提示

意味着应用程序中的每个状态片段都必须进入 Redux 存储!您应该根据状态在何处需要,决定状态片段是属于 Redux 还是 UI 组件。

状态是只读的

更改状态的唯一方法是分发一个action,它是一个描述发生事件的对象。

这样,UI 不会意外地覆盖数据,并且更容易追踪状态更新的原因。由于 action 是普通的 JS 对象,它们可以被记录、序列化、存储,并在以后用于调试或测试目的。

使用纯 Reducer 函数进行更改

为了指定如何根据 action 更新状态树,您需要编写reducer 函数。Reducer 是纯函数,它们接受先前状态和一个 action,并返回下一个状态。与任何其他函数一样,您可以将 reducer 分割成更小的函数来帮助完成工作,或者为常见任务编写可重用的 reducer。

Redux 应用程序数据流

之前,我们谈到了“单向数据流”,它描述了更新应用程序的这一系列步骤

  • 状态描述了应用程序在特定时间点的状况
  • UI 基于该状态进行渲染
  • 当发生某些事情(例如用户单击按钮)时,状态会根据发生的事情进行更新
  • UI 基于新状态重新渲染

具体到 Redux,我们可以将这些步骤分解得更详细

  • 初始设置
    • 使用根 reducer 函数创建一个 Redux store
    • store 会调用根 reducer 一次,并将返回值保存为其初始 state
    • 当 UI 首次渲染时,UI 组件会访问 Redux store 的当前状态,并使用该数据来决定要渲染的内容。它们还会订阅任何未来的 store 更新,以便它们知道状态是否发生了变化。
  • 更新
    • 应用程序中发生了一些事情,例如用户点击了一个按钮
    • 应用程序代码向 Redux store 分发一个 action,例如 dispatch({type: 'counter/incremented'})
    • store 会再次运行 reducer 函数,使用先前的 state 和当前 action,并将返回值保存为新的 state
    • store 会通知所有订阅了 store 更新的 UI 部分,store 已更新
    • 每个需要从 store 获取数据的 UI 组件都会检查它们需要的状态部分是否发生了变化。
    • 每个看到其数据已更改的组件都会强制使用新数据重新渲染,以便它可以更新屏幕上显示的内容

以下是该数据流的视觉效果

Redux data flow diagram

您学到了什么

总结
  • Redux 的意图可以概括为三个原则
    • 全局应用程序状态保存在单个存储中
    • 存储状态对于应用程序的其余部分是只读的
    • 使用 reducer 函数来响应操作更新状态
  • Redux 使用“单向数据流”应用程序结构
    • 状态描述了应用程序在某个时间点的状况,UI 根据该状态进行渲染
    • 当应用程序中发生某些事情时
      • UI 派发一个操作
      • 存储运行 reducer,并且状态根据发生的事情进行更新
      • 存储通知 UI 状态已更改
    • UI 基于新状态重新渲染

下一步?

您现在应该熟悉描述 Redux 应用程序不同部分的关键概念和术语。

现在,让我们看看这些部分是如何协同工作的,因为我们开始在 第 3 部分:状态、操作和 reducer 中构建一个新的 Redux 应用程序。