Redux Essentials,第一部分:Redux 概述和概念
- 什么是 Redux 以及为什么你可能想要使用它
- Redux 的关键术语和概念
- 数据如何在 Redux 应用中流动
简介
欢迎来到 Redux Essentials 教程!本教程将向你介绍 Redux,并教你如何使用它,使用我们最新的推荐工具和最佳实践。当你完成本教程后,你应该能够使用你在这里学到的工具和模式开始构建自己的 Redux 应用程序。
在本教程的第 1 部分中,我们将介绍使用 Redux 所需的关键概念和术语,在第 2 部分:Redux 应用结构中,我们将检查一个基本的 React + Redux 应用,以了解各个部分如何组合在一起。
从第 3 部分:基本 Redux 数据流开始,我们将使用这些知识构建一个小型社交媒体提要应用程序,其中包含一些现实世界的功能,了解这些部分在实践中是如何工作的,并讨论一些使用 Redux 的重要模式和指南。
如何阅读本教程
本页面将重点介绍如何正确使用 Redux,并解释足够的概念,以便你了解如何正确构建 Redux 应用程序。
我们已尽力使这些解释对初学者友好,但我们确实需要对您已经了解的内容做出一些假设。
- 熟悉HTML & CSS.
- 熟悉ES2015 语法和特性
- 了解 React 术语:JSX、状态、函数组件、属性和钩子
- 了解异步 JavaScript和发出 AJAX 请求
如果您还没有熟悉这些主题,我们建议您先花些时间熟悉它们,然后再回来学习 Redux。我们会在您准备好时等候您!
您应该确保已在浏览器中安装了 React 和 Redux DevTools 扩展程序
- React DevTools 扩展程序
- Redux DevTools 扩展程序
什么是 Redux?
首先,了解一下这个“Redux”是什么。它做什么?它能帮助我解决什么问题?为什么我要使用它?
Redux 是一种用于管理和更新应用程序状态的模式和库,它使用称为“操作”的事件。它充当整个应用程序中需要使用的状态的集中式存储,并通过规则确保状态只能以可预测的方式更新。
为什么要使用 Redux?
Redux 帮助您管理“全局”状态 - 在应用程序的许多部分中都需要使用的状态。
Redux 提供的模式和工具使您更容易理解何时、何地、为什么以及如何更新应用程序中的状态,以及当这些更改发生时您的应用程序逻辑将如何表现。Redux 指导您编写可预测且可测试的代码,这有助于您确信您的应用程序将按预期工作。
何时应该使用 Redux?
Redux 帮助您处理共享状态管理,但与任何工具一样,它也有权衡取舍。需要学习更多概念,并编写更多代码。它还会在您的代码中添加一些间接性,并要求您遵循某些限制。这是短期和长期生产力之间的权衡。
Redux 在以下情况下更有用
- 您有大量应用程序状态需要在应用程序的许多地方使用
- 应用程序状态会随着时间的推移而频繁更新
- 更新该状态的逻辑可能很复杂
- 应用程序具有中等或大型代码库,并且可能由许多人进行开发
并非所有应用程序都需要 Redux。花点时间思考一下你正在构建的应用程序类型,并决定哪些工具最适合解决你正在处理的问题。
如果你不确定 Redux 是否适合你的应用程序,以下资源可以提供更多指导。
Redux 库和工具
Redux 是一个小型独立的 JS 库。但是,它通常与其他几个包一起使用。
React-Redux
Redux 可以与任何 UI 框架集成,并且最常与 React 一起使用。 React-Redux 是我们的官方包,它允许你的 React 组件通过读取状态片段和分派操作来更新存储,从而与 Redux 存储进行交互。
Redux Toolkit
Redux Toolkit 是我们推荐的编写 Redux 逻辑的方法。它包含我们认为对构建 Redux 应用程序至关重要的包和函数。Redux Toolkit 内置了我们建议的最佳实践,简化了大多数 Redux 任务,防止了常见的错误,并使编写 Redux 应用程序变得更加容易。
Redux DevTools 扩展
The Redux DevTools 扩展 显示了随着时间的推移,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 术语。
操作
操作是一个简单的 JavaScript 对象,它具有一个 type
字段。您可以将操作视为描述应用程序中发生的事件的事件。
type
字段应是一个字符串,为该操作提供一个描述性名称,例如 "todos/todoAdded"
。我们通常将该类型字符串写成 "domain/eventName"
,其中第一部分是该操作所属的功能或类别,第二部分是发生的具体事件。
操作对象可以具有其他字段,其中包含有关发生事件的附加信息。按照惯例,我们将该信息放在名为 payload
的字段中。
一个典型的操作对象可能如下所示
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
操作创建者
操作创建者是一个创建并返回操作对象的函数。我们通常使用它们,这样我们就不必每次都手动编写操作对象。
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
减少器
减少器是一个函数,它接收当前 state
和一个 action
对象,决定是否需要更新状态,并返回新状态:(state, action) => newState
。您可以将减少器视为一个事件监听器,它根据接收到的操作(事件)类型处理事件。
"Reducer" 函数之所以得名,是因为它们类似于您传递给 Array.reduce()
方法的回调函数。
Reducer 必须始终遵循一些特定规则
- 它们应该只根据
state
和action
参数计算新的状态值 - 它们不允许修改现有的
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/increment') {
// 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/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
对象),根据这些参数决定一个新的状态值,并返回该新状态。
如果我们要创建一个Redux动作数组,调用reduce()
,并传入一个reducer函数,我们将以相同的方式获得最终结果
const actions = [
{ type: 'counter/increment' },
{ type: 'counter/increment' },
{ type: 'counter/increment' }
]
const initialState = { value: 0 }
const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}
我们可以说Redux reducers将一组动作(随着时间的推移)减少为单个状态。区别在于,使用Array.reduce()
,它会一次性完成,而使用Redux,它会在应用程序运行的整个生命周期中完成。
存储
当前Redux应用程序状态存储在一个名为store的对象中。
存储通过传入一个reducer来创建,并且有一个名为getState
的方法,该方法返回当前状态值
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
调度
Redux存储有一个名为dispatch
的方法。更新状态的唯一方法是调用store.dispatch()
并传入一个动作对象。存储将运行其reducer函数并将新的状态值保存在内部,我们可以调用getState()
来检索更新后的值
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
您可以将调度动作视为在应用程序中“触发事件”。发生了某些事情,我们希望存储知道它。Reducers就像事件监听器,当它们听到它们感兴趣的动作时,它们会相应地更新状态。
我们通常调用动作创建者来调度正确的动作
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
选择器
选择器是能够从存储状态值中提取特定信息的函数。随着应用程序的规模越来越大,这可以帮助避免在应用程序的不同部分需要读取相同数据时重复逻辑。
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
Redux 应用程序数据流
之前,我们谈到了“单向数据流”,它描述了更新应用程序的这一系列步骤。
- 状态描述了应用程序在特定时间点的状况。
- UI 基于该状态进行渲染。
- 当发生某些事情(例如用户点击按钮)时,状态会根据发生的事情进行更新。
- UI 根据新状态重新渲染。
具体到 Redux,我们可以将这些步骤细化。
- 初始设置
- 使用根 reducer 函数创建一个 Redux 存储。
- 存储调用根 reducer 一次,并将返回值保存为其初始
state
。 - 当 UI 首次渲染时,UI 组件访问 Redux 存储的当前状态,并使用该数据来决定要渲染的内容。它们还订阅任何未来的存储更新,以便它们可以知道状态是否已更改。
- 更新
- 应用程序中发生了一些事情,例如用户点击了一个按钮。
- 应用程序代码向 Redux 存储分派一个操作,例如
dispatch({type: 'counter/increment'})
。 - 存储使用先前的
state
和当前action
再次运行 reducer 函数,并将返回值保存为新的state
。 - 存储通知所有订阅了存储更新的 UI 部分,表明存储已更新。
- 每个需要从存储中获取数据的 UI 组件都会检查它们需要的状态部分是否已更改。
- 每个看到其数据已更改的组件都会强制使用新数据重新渲染,以便它可以更新屏幕上显示的内容。
以下是该数据流的视觉效果。
您学到了什么
Redux 确实有一些新的术语和概念需要记住。作为提醒,以下是我们刚刚介绍的内容。
- Redux 是一个用于管理全局应用程序状态的库。
- Redux 通常与 React-Redux 库一起使用,用于将 Redux 和 React 集成在一起。
- Redux Toolkit 是编写 Redux 逻辑的推荐方法。
- Redux 使用“单向数据流”应用程序结构。
- 状态描述了应用程序在某个时间点的状况,UI 根据该状态进行渲染。
- 当应用程序中发生某些事件时
- UI 会派发一个 action
- store 会运行 reducers,并根据发生的事件更新状态
- store 会通知 UI 状态已更改
- UI 根据新状态重新渲染。
- Redux 使用几种类型的代码
- Action 是带有
type
字段的普通对象,描述了应用程序中“发生了什么”。 - Reducers 是根据先前状态和 action 计算新状态值的函数。
- Redux store 在每次 dispatch action 时都会运行根 reducer。
- Action 是带有
下一步?
我们已经看到了 Redux 应用程序的各个部分。接下来,请继续访问 第 2 部分:Redux Toolkit 应用程序结构,我们将查看一个完整的运行示例,以了解各个部分如何组合在一起。