Skip to main content

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

你将学到什么
  • 使用 Redux 的关键术语和概念

    ¥Key terms and concepts for using Redux

  • 数据如何流经 Redux 应用

    ¥How data flows through a Redux app

介绍

¥Introduction

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

¥In Part 1: Redux Overview, we talked about what Redux is, why you might want to use it, and listed the other Redux libraries that are typically used with the Redux core. We also saw a small example of what a working Redux app looks like and the pieces that make up the app. Finally, we briefly mentioned some of the terms and concepts used with Redux.

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

¥In this section, we'll look at those terms and concepts in more detail, and talk more about how data flows through a Redux application.

提醒

请注意,本教程有意展示旧式 Redux 逻辑模式,这些模式比我们使用 Redux Toolkit 教授的 "现代回归" 模式需要更多的代码,作为当今使用 Redux 构建应用的正确方法,以便解释 Redux 背后的原理和概念。它并不意味着是一个生产就绪的项目。

¥Note that this tutorial intentionally shows older-style Redux logic patterns that require more code than the "modern Redux" patterns with Redux Toolkit we teach as the right approach for building apps with Redux today, in order to explain the principles and concepts behind Redux. It's not meant to be a production-ready project.

请参阅以下页面以了解如何将 "现代回归" 与 Redux Toolkit 结合使用:

¥See these pages to learn how to use "modern Redux" with Redux Toolkit:

背景概念

¥Background Concepts

在深入研究实际代码之前,让我们先讨论一下使用 Redux 时需要了解的一些术语和概念。

¥Before we dive into some actual code, let's talk about some of the terms and concepts you'll need to know to use Redux.

状态管理

¥State Management

让我们首先看一个小型 React 计数器组件。它跟踪组件状态中的数字,并在单击按钮时增加该数字:

¥Let's start by looking at a small React counter component. It tracks a number in component state, and increments the number when a button is clicked:

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>
)
}

它是一个独立的应用,包含以下部分:

¥It is a self-contained app with the following parts:

  • 状态,驱动我们应用的真相来源;

    ¥The state, the source of truth that drives our app;

  • 视图,基于当前状态的 UI 声明性描述

    ¥The view, a declarative description of the UI based on the current state

  • 应用中基于用户输入发生的操作、事件以及触发状态更新

    ¥The actions, the events that occur in the app based on user input, and trigger updates in the state

这是 "单向数据流" 的一个小例子:

¥This is a small example of "one-way data flow":

  • 状态描述了应用在特定时间点的状况

    ¥State describes the condition of the app at a specific point in time

  • UI 根据该状态渲染

    ¥The UI is rendered based on that state

  • 当发生某些事情时(例如用户单击按钮),状态会根据发生的情况进行更新

    ¥When something happens (such as a user clicking a button), the state is updated based on what occurred

  • UI 根据新状态重新渲染

    ¥The UI re-renders based on the new state

One-way data flow

但是,当我们有多个需要共享和使用相同状态的组件时,特别是当这些组件位于应用的不同部分时,这种简单性可能会被打破。有时这可以通过 "提升状态" 到父组件来解决,但这并不总是有帮助。

¥However, the simplicity can break down when we have multiple components that need to share and use the same state, especially if those components are located in different parts of the application. Sometimes this can be solved by "lifting state up" to parent components, but that doesn't always help.

解决此问题的一种方法是从组件中提取共享状态,并将其放入组件树外部的集中位置。这样,我们的组件树就变成了一个大 "view",任何组件都可以访问状态或触发操作,无论它们位于树中的哪个位置!

¥One way to solve this is to extract the shared state from the components, and put it into a centralized location outside the component tree. With this, our component tree becomes a big "view", and any component can access the state or trigger actions, no matter where they are in the tree!

通过定义和分离状态管理中涉及的概念以及执行维护视图和状态之间独立性的规则,我们为代码提供了更多的结构和可维护性。

¥By defining and separating the concepts involved in state management and enforcing rules that maintain independence between views and states, we give our code more structure and maintainability.

这是 Redux 背后的基本思想:一个集中的位置,用于包含应用中的全局状态,以及更新该状态以使代码可预测时要遵循的特定模式。

¥This is the basic idea behind Redux: a single centralized place to contain the global state in your application, and specific patterns to follow when updating that state to make the code predictable.

不可变性

¥Immutability

"可变的" 表示 "changeable"。如果某件事是 "immutable",它就永远无法改变。

¥"Mutable" means "changeable". If something is "immutable", it can never be changed.

默认情况下,JavaScript 对象和数组都是可变的。如果我创建一个对象,我可以更改其字段的内容。如果我创建一个数组,我也可以更改内容:

¥JavaScript objects and arrays are all mutable by default. If I create an object, I can change the contents of its fields. If I create an array, I can change the contents as well:

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'

这称为改变对象或数组。它是内存中相同的对象或数组引用,但现在对象内部的内容已更改。

¥This is called mutating the object or array. It's the same object or array reference in memory, but now the contents inside the object have changed.

为了不可变地更新值,你的代码必须复制现有对象/数组,然后修改副本。

¥In order to update values immutably, your code must make copies of existing objects/arrays, and then modify the copies.

我们可以使用 JavaScript 的数组/对象扩展运算符以及返回数组新副本而不是改变原始数组的数组方法来手动完成此操作:

¥We can do this by hand using JavaScript's array / object spread operators, as well as array methods that return new copies of the array instead of mutating the original array:

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 期望所有状态更新都是不可变地完成的。稍后我们将了解这一点在何处以及如何重要,以及一些编写不可变更新逻辑的更简单的方法。

¥Redux expects that all state updates are done immutably. We'll look at where and how this is important a bit later, as well as some easier ways to write immutable update logic.

想知道更多?

有关 JavaScript 中不可变性如何工作的更多信息,请参阅:

¥For more info on how immutability works in JavaScript, see:

Redux 术语

¥Redux Terminology

在我们继续之前,你需要熟悉一些重要的 Redux 术语:

¥There's some important Redux terms that you'll need to be familiar with before we continue:

行动

¥Actions

操作是一个具有 type 字段的纯 JavaScript 对象。你可以将操作视为描述应用中发生的事情的事件。

¥An action is a plain JavaScript object that has a type field. You can think of an action as an event that describes something that happened in the application.

type 字段应该是一个字符串,为该操作提供一个描述性名称,例如 "todos/todoAdded"。我们通常会写像 "domain/eventName" 这样的类型字符串,其中第一部分是这个动作所属的特性或类别,第二部分是发生的具体事情。

¥The type field should be a string that gives this action a descriptive name, like "todos/todoAdded". We usually write that type string like "domain/eventName", where the first part is the feature or category that this action belongs to, and the second part is the specific thing that happened.

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

¥An action object can have other fields with additional information about what happened. By convention, we put that information in a field called payload.

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

¥A typical action object might look like this:

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

Reducer

reducer 是一个函数,它接收当前的 stateaction 对象,决定如何在必要时更新状态,并返回新状态:(state, action) => newState。你可以将 reducer 视为事件监听器,它根据接收到的操作(事件)类型来处理事件。

¥A reducer is a function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state: (state, action) => newState. You can think of a reducer as an event listener which handles events based on the received action (event) type.

信息

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

¥"Reducer" functions get their name because they're similar to the kind of callback function you pass to the Array.reduce() method.

reducer 必须始终遵循一些特定的规则:

¥Reducers must always follow some specific rules:

  • 他们应该只根据 stateaction 参数计算新的状态值

    ¥They should only calculate the new state value based on the state and action arguments

  • 他们不得修改现有的 state。相反,他们必须通过复制现有的 state 并对复制的值进行更改来进行不可变的更新。

    ¥They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.

  • 它们不得执行任何异步逻辑、计算随机值或导致其他 "副作用"

    ¥They must not do any asynchronous logic, calculate random values, or cause other "side effects"

稍后我们将更多地讨论 reducer 的规则,包括它们为何重要以及如何正确遵循它们。

¥We'll talk more about the rules of reducers later, including why they're important and how to follow them correctly.

reducer 函数内部的逻辑通常遵循相同的一系列步骤:

¥The logic inside reducer functions typically follows the same series of steps:

  • 检查 reducer 是否关心这个 action

    ¥Check to see if the reducer cares about this action

    • 如果是,则复制状态,用新值更新副本,然后返回

      ¥If so, make a copy of the state, update the copy with new values, and return it

  • 否则,返回现有状态不变

    ¥Otherwise, return the existing state unchanged

这是一个 reducer 的小示例,显示了每个 reducer 应遵循的步骤:

¥Here's a small example of a reducer, showing the steps that each reducer should follow:

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、循环等等。

¥Reducers can use any kind of logic inside to decide what the new state should be: if/else, switch, loops, and so on.

Detailed Explanation: Why Are They Called 'Reducers?'

Array.reduce() 方法允许你获取一个值数组,一次处理数组中的每一项,并返回一个最终结果。你可以把它想象成 "将数组减少到一个值"。

¥The Array.reduce() method lets you take an array of values, process each item in the array one at a time, and return a single final result. You can think of it as "reducing the array down to one value".

Array.reduce() 采用回调函数作为参数,该函数将为数组中的每一项调用一次。它需要两个参数:

¥Array.reduce() takes a callback function as an argument, which will be called one time for each item in the array. It takes two arguments:

  • previousResult,你上次回调返回的值

    ¥previousResult, the value that your callback returned last time

  • currentItem,数组中的当前项

    ¥currentItem, the current item in the array

回调第一次运行时,没有可用的 previousResult,因此我们还需要传入一个初始值,该值将用作第一个 previousResult

¥The first time that the callback runs, there isn't a previousResult available, so we need to also pass in an initial value that will be used as the first previousResult.

如果我们想将一组数字相加来找出总数,我们可以编写一个如下所示的 reduce 回调:

¥If we wanted to add together an array of numbers to find out what the total is, we could write a reduce callback that looks like this:

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 "减少回调" 函数本身不需要跟踪任何内容。它接受 previousResultcurrentItem 参数,对它们执行某些操作,然后返回一个新的结果值。

¥Notice that this addNumbers "reduce callback" function doesn't need to keep track of anything itself. It takes the previousResult and currentItem arguments, does something with them, and returns a new result value.

Redux reducer 函数与这个 "减少回调" 函数的想法完全相同!它采用 "之前的结果"(state)和 "当前项目"(action 对象),根据这些参数决定新的状态值,并返回该新状态。

¥A Redux reducer function is exactly the same idea as this "reduce callback" function! It takes a "previous result" (the state), and the "current item" (the action object), decides a new state value based on those arguments, and returns that new state.

如果我们要创建一个 Redux actions 数组,调用 reduce(),并传入一个 reducer 函数,我们会以同样的方式得到最终结果:

¥If we were to create an array of Redux actions, call reduce(), and pass in a reducer function, we'd get a final result the same way:

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,它会在正在运行的应用的生命周期内发生。

¥We can say that Redux reducers reduce a set of actions (over time) into a single state. The difference is that with Array.reduce() it happens all at once, and with Redux, it happens over the lifetime of your running app.

存储

¥Store

当前的 Redux 应用状态位于一个名为 store 的对象中。

¥The current Redux application state lives in an object called the store .

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

¥The store is created by passing in a reducer, and has a method called getState that returns the current state value:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

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

派遣

¥Dispatch

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

¥The Redux store has a method called dispatch. The only way to update the state is to call store.dispatch() and pass in an action object. The store will run its reducer function and save the new state value inside, and we can call getState() to retrieve the updated value:

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

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

你可以将应用中的调度操作视为 "触发事件"。发生了一些事情,我们希望存储知道这件事。reducer 就像事件监听器一样,当它们听到感兴趣的操作时,它们会更新状态作为响应。

¥You can think of dispatching actions as "triggering an event" in the application. Something happened, and we want the store to know about it. Reducers act like event listeners, and when they hear an action they are interested in, they update the state in response.

选择器

¥Selectors

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

¥Selectors are functions that know how to extract specific pieces of information from a store state value. As an application grows bigger, this can help avoid repeating logic as different parts of the app need to read the same data:

const selectCounterValue = state => state.value

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

核心概念和原则

¥Core Concepts and Principles

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

¥Overall, we can summarize the intent behind Redux's design in three core concepts:

单一事实来源

¥Single Source of Truth

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

¥The global state of your application is stored as an object inside a single store. Any given piece of data should only exist in one location, rather than being duplicated in many places.

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

¥This makes it easier to debug and inspect your app's state as things change, as well as centralizing logic that needs to interact with the entire application.

提示

这并不意味着应用中的每个状态都必须进入 Redux 存储!你应该根据需要决定某个状态是属于 Redux 还是属于你的 UI 组件。

¥This does not mean that every piece of state in your app must go into the Redux store! You should decide whether a piece of state belongs in Redux or your UI components, based on where it's needed.

状态为只读

¥State is Read-Only

改变状态的唯一方法是调度一个动作,一个描述发生了什么的对象。

¥The only way to change the state is to dispatch an action, an object that describes what happened.

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

¥This way, the UI won't accidentally overwrite data, and it's easier to trace why a state update happened. Since actions are plain JS objects, they can be logged, serialized, stored, and later replayed for debugging or testing purposes.

使用纯 Reducer 函数进行更改

¥Changes are Made with Pure Reducer Functions

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

¥To specify how the state tree is updated based on actions, you write reducer functions. Reducers are pure functions that take the previous state and an action, and return the next state. Like any other functions, you can split reducers into smaller functions to help do the work, or write reusable reducers for common tasks.

Redux 应用数据流

¥Redux Application Data Flow

之前,我们讨论了 "单向数据流",它描述了更新应用的步骤顺序:

¥Earlier, we talked about "one-way data flow", which describes this sequence of steps to update the app:

  • 状态描述了应用在特定时间点的状况

    ¥State describes the condition of the app at a specific point in time

  • UI 根据该状态渲染

    ¥The UI is rendered based on that state

  • 当发生某些事情时(例如用户单击按钮),状态会根据发生的情况进行更新

    ¥When something happens (such as a user clicking a button), the state is updated based on what occurred

  • UI 根据新状态重新渲染

    ¥The UI re-renders based on the new state

特别是对于 Redux,我们可以将这些步骤分解为更详细的内容:

¥For Redux specifically, we can break these steps into more detail:

  • 初始设置:

    ¥Initial setup:

    • Redux 存储是使用根 reducer 函数创建的

      ¥A Redux store is created using a root reducer function

    • store 调用一次根 reducer,并将返回值保存为其初始 state

      ¥The store calls the root reducer once, and saves the return value as its initial state

    • 当 UI 首次渲染时,UI 组件访问 Redux 存储的当前状态,并使用该数据来决定渲染什么。他们还订阅任何未来的存储更新,以便他们知道状态是否发生变化。

      ¥When the UI is first rendered, UI components access the current state of the Redux store, and use that data to decide what to render. They also subscribe to any future store updates so they can know if the state has changed.

  • 更新:

    ¥Updates:

    • 应用中发生了一些事情,例如用户单击按钮

      ¥Something happens in the app, such as a user clicking a button

    • 应用代码将一个操作分派到 Redux 存储,例如 dispatch({type: 'counter/incremented'})

      ¥The app code dispatches an action to the Redux store, like dispatch({type: 'counter/incremented'})

    • store 用之前的 state 和当前的 action 再次运行 reducer 函数,并将返回值保存为新的 state

      ¥The store runs the reducer function again with the previous state and the current action, and saves the return value as the new state

    • 存储通知订阅的 UI 的所有部分该存储已更新

      ¥The store notifies all parts of the UI that are subscribed that the store has been updated

    • 每个需要存储中数据的 UI 组件都会检查它们所需的状态部分是否已更改。

      ¥Each UI component that needs data from the store checks to see if the parts of the state they need have changed.

    • 每个看到其数据已更改的组件都会强制使用新数据重新渲染,因此它可以更新屏幕上显示的内容

      ¥Each component that sees its data has changed forces a re-render with the new data, so it can update what's shown on the screen

数据流的视觉效果如下:

¥Here's what that data flow looks like visually:

Redux data flow diagram

你学到了什么

¥What You've Learned

概括
  • Redux 的意图可以概括为三个原则

    ¥Redux's intent can be summarized in three principles

    • 全局应用状态保存在单个存储中

      ¥Global app state is kept in a single store

    • 存储状态对于应用的其余部分是只读的

      ¥The store state is read-only to the rest of the app

    • reducer 函数用于更新状态以响应操作

      ¥Reducer functions are used to update the state in response to actions

  • Redux 使用 "单向数据流" 应用结构

    ¥Redux uses a "one-way data flow" app structure

    • 状态描述了应用在某个时间点的状况,UI 根据该状态进行渲染

      ¥State describes the condition of the app at a point in time, and UI renders based on that state

    • 当应用中发生某些情况时:

      ¥When something happens in the app:

      • UI 调度一个动作

        ¥The UI dispatches an action

      • 存储运行 reducer,并根据发生的情况更新状态

        ¥The store runs the reducers, and the state is updated based on what occurred

      • 存储通知 UI 状态已更改

        ¥The store notifies the UI that the state has changed

    • UI 根据新状态重新渲染

      ¥The UI re-renders based on the new state

下一步是什么?

¥What's Next?

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

¥You should now be familiar with the key concepts and terms that describe the different parts of a Redux app.

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

¥Now, let's see how those pieces work together as we start building a new Redux application in Part 3: State, Actions, and Reducers.