介绍
欢迎使用Redux必须手册!本教程将向你介绍Redux,并教你如何以正确的方式使用我们推荐的最新工具和最佳实践。在完成教程时,你应该能够使用在这里学到的工具和模式,来开始构建自己的Redux应用程序。
本教程的第1部分中,我们将介绍使用Redux所需要了解的关键概念和术语,在第2部分:Redux应用程序结构中,我们将研究一个基本的React + Redux应用程序,以了解各部分之间的组合方式。
从第3部分:基本Redux数据流开始,我们将使用该知识来构建具有一些实际功能的小型社交媒体供稿应用程序,了解这些部分在实际中的实际工作方式,并讨论一些Redux中重要的使用模式和准则。
如何阅读本教程
该页面将着重于向你展示如何正确使用Redux,并解释了足够的概念,以便你了解如何正确构建Redux应用程序。
我们试图使这些说明适合于初学者,但是我们确实需要对你已经了解的内容做出一些假设:
前提条件
- 熟悉HTML和CSS。
- 熟悉ES6语法和功能。
- 了解React术语:JSX,State,函数组件,Props,和Hook。
- 了解异步JavaScript和发出AJAX请求。
如果你还不熟悉这些主题,建议你花一些时间先熟悉一下这些主题,然后再回来学习Redux。 当你准备就绪时,我们始终在这里!
你应该确保在浏览器中安装了React和Redux DevTools扩展:
- React DevTools扩展:
- Redux DevTools扩展:
什么是Redux?
首先,它有助于理解“Redux”的含义。它有什么作用?可以帮助我解决什么问题?我为什么要使用它?
Redux是一种模式和库,用于使用称为“action”的事件来管理和更新应用程序state。 它用作需要在整个应用程序中使用的state的集中存储,其规则确保只能以可预测的方式更新state。
我为什么需要使用Redux?
Redux帮助你管理“全局”state——应用程序许多部分所需的state。
Redux提供的模式和工具使你更容易理解何时,何地,为什么以及如何更新应用程序中的state,以及发生这些更改时应用程序的逻辑的行为。 Redux指导你编写可预测和可测试的代码,这有助于使你确信应用程序将按预期运行。
我什么时候需要使用Redux?
Redux可以帮助你处理共享state管理,但是像其他任何工具一样,它也需要权衡取舍。有更多的概念需要学习,还有更多的代码需要编写。它还会在代码中添加一些间接性,并要求你遵循某些限制。这是短期效率和长期生产率之间的权衡。
在以下情况下,Redux更为有用:
- 你在应用程序的许多地方都有大量的应用程序state
- 应用state会随着时间的推移而频繁更新
- 更新该state的逻辑可能很复杂
- 该应用程序具有中型或大型代码库,可能被许多人使用
并非所有应用都需要Redux。花一些时间考虑一下你正在构建的应用程序的类型,并确定哪种工具最能帮助你解决正在解决的问题。
想知道更多?
如果不确定Redux是否适合您的应用,这些资源将提供更多指导:
Redux库和工具
Redux是一个小的独立JS库。但是,它通常与其他几个软件包一起使用:
React-Redux
Redux可以与任何UI框架集成,并且最常与React一起使用。React-Redux是我们的官方软件包,使你的React组件可以通过读取state slice并dispatch action来更新store来与Redux store进行交互。
Redux Toolkit
Redux Toolkit是我们推荐的编写Redux逻辑的方法。它包含我们认为对构建Redux应用至关重要的软件包和功能。Redux Toolkit建立在我们建议的最佳实践中,简化了大多数Redux任务,防止了常见错误,并使编写Redux应用程序更加容易。
Redux DevTools Extension
Redux DevTools Extension显示了Redux store中state随时间变化的历史记录。这使你可以有效地调试应用程序,包括使用诸如“时间旅行调试”之类的强大技术。
Redux术语和概念
在深入研究一些实际代码之前,让我们谈谈使用Redux所需了解的一些术语和概念。
State管理
让我们从一个小的React计数器组件开始。它跟踪组件state下的数字,并在单击按钮时递增数字:
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>
)
}
复制代码
这是一个包含以下部分的自包含应用程序:
- state,驱动我们应用程序的真理之源;
- view,基于当前state的UI声明式描述;
- action,基于用户输入在应用程序中发生的事件,以及触发state更新;
这是“单向数据流”的一个小示例:
- state描述了特定时间点的应用程序状态
- UI根据该state呈现
- 发生某些情况(例如用户单击按钮)时,将根据发生的情况更新state
- UI根据新state重新呈现
但是,当我们有多个需要共享和使用同一state的组件时,尤其是当这些组件位于应用程序的不同部分时,其简单性可能会崩溃。有时可以通过“提升state”到父组件来解决,但这并不总是有帮助的。
解决此问题的一种方法是从组件中提取共享state,并将其放入组件树外部的集中位置。这样,我们的组件树将变成一个很大的“view”,并且任何组件都可以访问state或触发action,无论它们在树中的位置!
通过定义和分离state管理中涉及的概念并执行维持view和state之间独立性的原则,我们为代码提供了更多的结构和可维护性。
这是Redux背后的基本思想:一个单一的集中位置,来包含应用程序中的全局state,以及使用特定的模式来跟踪state的更新,使代码逻辑和结果可预测。
不可变性
“可变”是指“可改变的”。如果某些东西是“不可变的”,则永远无法更改。
默认情况下,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期望所有state更新都是一成不变的。 稍后,我们将探讨这在何处以及如何变得很重要,以及一些更简单的方法来编写不可变的对象/数组的更新逻辑。
想知道更多?
有关不可变性如何在JavaScript中工作的更多信息,请参见:
术语
在继续之前,你需要熟悉一些重要的Redux术语:
Action
Action是具有type
字段的普通JavaScript对象。你可以将action视为描述应用程序中发生的事情的事件。
type
字段应该是一个为该action赋予描述性名称的字符串,例如"todos/todoAdded"
。我们通常将其写为"domain/eventName"
之类的字符串,其中第一部分是该action所属的功能或类别,第二部分是发生的特定事件。
Action对象可以具有其他字段,其中包含有关发生的事件的附加信息。按照惯例,我们将该信息放在一个称为payload
的字段中。
一个典型的action对象可能看起来像这样:
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
复制代码
Action Creator
Action creator是创建并返回action对象的函数。通常,我们使用代码创建这些对象,因此不必每次都手动编写action对象:
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
复制代码
Reducer
Reducer是一个函数,它接收当前state和一个action对象,并在必要时决定如何更新state,并返回新state:(state, action) => newState
。你可以将reducer视为事件侦听器,该事件侦听器根据接收到的action(事件)type来处理事件。
信息
之所以使用“reducer”函数,是因为它们类似于你传递给
Array.reduce()
方法的那种回调函数。
Reducer必须始终遵循一些特定规则:
- 它们应该仅根据
state
和action
参数来计算新的状态值 - 不允许它们修改现有state。相反,它们必须通过复制现有state并对复制的值进行更改,来进行不可变的更新。
- 它们不得执行任何异步逻辑,计算随机值或引起其他“副作用”
稍后,我们将详细讨论reducer的规则,包括其重要性以及如何正确遵循它们。
Reducer函数内部的逻辑通常遵循以下一系列步骤:
- 检查reducer是否关心此action
- 如果是这样,请复制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/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可以使用内部的任何逻辑来决定新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 action数组,调用
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 reducer将一组action(随着时间的推移)减少到单个state。区别在于,使用
Array.reduce()
是一次全部发生,而使用Redux则可以在运行的应用程序的整个生命周期内发生。
Store
当前的Redux应用程序state存在于一个称为store的对象中。
该store是通过传入一个reducer来创建的,并且具有一个名为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/increment' })
console.log(store.getState())
// {value: 1}
复制代码
你可以在应用程序中将dispatch action视为“触发事件”。 发生了什么事,我们希望store知道这一点。 reducer的行为类似于事件侦听器,当他们听到自己感兴趣的action时,就会响应该state来更新state。
我们通常会要求action creator dispatch正确的action:
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
复制代码
Selector
Selector是知道如何从store的state值中提取特定信息的函数。随着应用程序变得更大,这可以避免重复逻辑,因为应用程序的不同部分需要读取相同的数据:
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
复制代码
Redux应用程序数据流
之前,我们讨论了“单向数据流”,它描述了更新应用程序的以下步骤序列:
- state描述了特定时间点的应用程序状态
- UI根据该state呈现
- 发生某些情况(例如用户单击按钮)时,将根据发生的情况更新state
- UI根据新state重新呈现
特别是对于Redux,我们可以将这些步骤分为更多细节:
- 初始设置:
- 使用根reducer函数创建Redux store
- store调用根reducer一次,并将返回值保存为其初始
state
- 首次呈现UI时,UI组件访问Redux store的当前state,并使用该数据来决定呈现什么。它们还订阅了将来的任何store更新,因此他们可以知道state是否已更改。
- 更新:
- 应用程序中发生了某些事情,例如用户单击按钮
- 应用程式代码会dispatch一个action到Redux store,例如
dispatch({type: 'counter/increment'})
- store以先前state和当前action再次运行reducer函数,并将返回值保存为新state
- store将通知已订阅的所有UI——部分store已更新
- 每个需要从store中获取数据的UI组件都会检查以查看其所需的state部分是否已更改。
- 每个看到其数据已更改的组件都会强制使用新数据重新渲染,因此它可以更新屏幕上显示的内容
数据流的可视化如下所示: