react课程13-Redux入门
Last updated on September 19, 2024 am
🥳本节课开始学习Redux
一、Redux的定义
Redux 是一个用于管理 JavaScript 应用中状态的开源库,通常与 React 一起使用。Redux 的核心思想是将应用的状态存储在一个全局的单一状态树中,这样应用中的任何组件都可以访问和更新该状态。
Redux 的三个核心原则:
单一数据源: 应用的所有状态保存在一个对象树中,并且这个状态对象树是只读的。
状态是只读的: 唯一改变状态的方法是发出一个 action,action 是一个描述事件的普通 JavaScript 对象。
使用纯函数来修改状态: 通过编写纯函数 reducers 来根据 action 描述的事件返回新的状态。
使用 Redux 的步骤:
创建 store:
createStore()
用于创建 Redux 的存储(store)。(含有多个Reducer,为每个应用程序特性或每个数据创建一个reducer)定义 reducer:reducer 是一个纯函数,接收当前的状态和 action,然后返回新的状态。
分发 action:组件可以通过
dispatch(action)
来发送 action,进而触发状态的变化。连接组件与 store:通过
connect()
或者 hooks(如useSelector
和useDispatch
)来让 React 组件与 Redux 的状态进行交互。
(1)什么是纯函数
纯函数(Pure Function)是指在相同的输入下总是返回相同输出,并且没有副作用的函数。
1、相同的输入,得到相同的输出:
一个纯函数依赖于它的输入参数,任何时候只要输入相同,输出就一定相同。例如,sum(a, b)
函数总是返回 a + b
,不论什么时候调用。
2、没有副作用:
纯函数不会修改外部的状态,也不会影响外部环境。比如它不会更改全局变量、修改传入的参数,也不会执行诸如 IO 操作、网络请求等副作用。
3、纯函数的优点:
- 易于测试和调试:由于纯函数依赖于输入和输出,没有副作用,因此它们容易进行单元测试。
- 可组合性:多个纯函数可以组合使用,减少了复杂度。
- 可预测性:因为纯函数不会依赖外部状态或产生副作用,它的行为更容易预测。
(2)为什么修改状态不算副作用
副作用指的是对外部环境的改变,例如修改外部的变量、执行 I/O 操作、调用 API 等,这些操作会导致应用的外部环境在函数运行前后发生变化。而 Redux 中的 reducer 是一个纯函数,它接收旧的状态和 action,并返回一个新的状态对象,而不是修改现有的状态。
不可变性:在 Redux 中,reducer 不直接修改传入的状态,而是创建并返回一个新的状态对象。这种方式保证了状态的不可变性,从而避免了副作用。
直接修改状态:
function reducer(state, action) {
state.count += 1;
return state; }
返回新状态:
1 |
|
二、学习Redux
⭕npm install redux
(1)建立Store
1、start
const store = createStore(reducer);
//建立store
store.dispatch({ type: "account/deposit", payload: 500 });
//发出动作
console.log(store.getState());
//获取状态值
2、working with action creators
function deposit( amount ) {
`return { type: "account/deposit", payload: amount };`
}
store.dispatch(deposit(500));
3、两个reducer
1 |
|
4、state slices(专业的状态结构)
slice文件存储与相关用户有关的状态以及action creactor函数;(export defaut reducer,export action creator 函数)
index.js文件:import store from "./store";
(2)将redux和react连接起来
⭕npm i react-redux
首先导入Provider
import { Provider } from "react-redux";
😋和context API很类似哦然后把app包裹进去:
1 |
|
接下来每个需要从Redux的store信息的组件都可以接收到了。
读取信息:
- 导入hook:
import { useSelector } from "react-redux";
- 读取信息:
const customer = useSelector( (store) => store.customer );
or
const customer = useSelector( (store) =>store.customer.fullName
(customer与在store.js
中的命名对应)- 导入hook:
1、dispatching actions from our react app
const dispatch=useDispatch();
得到dispath函数,这样dispatch就会按照平常那样工作啦,只需要传递所需的参数就可以了
PS:(解构value)(冒号前的是重构前的名字,冒号后是此文件要用的名字)
1 |
|
2、把组件连接到Redux的旧方法
mapStateToProps
是 Redux 中旧的(传统的)方法之一,用于将 Redux 的全局状态映射到 React 组件的 props 上,从而使组件能够访问 Redux 状态。在使用 mapStateToProps
的时候,通常搭配 connect
函数来将 Redux 状态和 React 组件连接起来。
例如:
1️⃣mapStateToProps函数的作用是从Redux的store中提取所需的状态,并通过props将这些状态传递给React组件,它接收两个参数:state、ownProps(可选,组件自身的props)
2️⃣使用connect接收mapStateToProps变成一个新的函数,这个函数接收Balance函数(想要连接的函数)作为参数,将状态传递给Balance组件
3️⃣Balance组件接收参数
(3)Middleware
要引入这个问题,首先要知道Reducer中不能含有像API调用这样的异步操作,那么这些操作应该放在哪里呢🤔?(在组件中分散地fetching data显然不理想 )(而store里显然也不行)
在 Redux 中,middleware(中间件) 是指一个扩展 Redux dispatch 功能的机制,允许你在发出 action 和 reducer 处理该 action 之间插入自定义的逻辑。它的主要作用是拦截 action,执行一些额外的处理,比如日志记录、异步请求、错误处理等。
1、中间件的作用
- 处理异步操作:Redux 本身只能处理同步的状态更新,而中间件可以让你执行异步操作,比如网络请求。常见的异步中间件有
redux-thunk
和redux-saga
。 - 记录日志:可以在每次 action 被触发时记录日志。
- 错误处理:在 action 到达 reducer 之前处理错误或异常情况。
- 自定义扩展:可以在
dispatch
过程中加入自定义的逻辑,比如修改 action、延迟 dispatch 等。
2、中间件的工作流程
Redux 的中间件基本上是一个函数,它接收 store
,然后返回一个函数,该函数接收 next
,最后返回一个函数处理 action
。中间件在每次 action 被派发时会执行,并且可以控制 action 是否传递到下一个中间件或 reducer。
中间件执行顺序如下:
dispatch(action)
-> 中间件链 -> reducer -> 更新 store
(4)Redux Thunk的应用
⭕npm i redux-thunk
1、进入store.js
做如下改动:
- 添加导入:
import thunk from "redux-thunk";
- 使用applyMiddleware创建store:
const store = createStore(rootReducer, applyMiddleware(thunk));
❓为什么导入thunk不需要花括号?
在 JavaScript 中,模块可以有两种类型的导出方式:
默认导出(default export):一个模块只能有一个默认导出,导入时可以使用任意名字,并且不需要花括号。
命名导出(named export):一个模块可以有多个命名导出,导入时必须使用花括号,且必须与导出时的名字匹配。
在 redux-thunk
这个库中,thunk
是默认导出的内容,所以导入时不需要花括号。
尴尬的是不加花括号报错了哈哈哈所以到底是默认还是命名啊……
2、在action creator函数中做异步API调动
要做API调用后返回action,因此先返回一个函数,React看到这个函数就会知道这是thunk,因此会先执行函数中的内容再返回action。
本次App需要用到的API如图所示,是一个钱币转换API,FROM:https://www.frankfurter.app/docs/
➡️fetch(https://api.frankfurter.app/latest?amount=10&from=GBP&to=USD
)
1 |
|
⁉️为什么上面的操作可以直接return action,下面却要dispatch action?
如果是一个普通的同步操作(如你代码中的
if (currency === "USD")
情况),你可以直接返回一个 action 对象,Redux会立即处理这个 action。但在异步操作中(如 API 调用时),你不能直接返回 action,因为 action 的payload
是异步获取的。此时,返回的不是普通的 action,而是一个异步的函数(即thunk
函数),这个函数需要等到异步操作完成后再手动派发 action,更新 Redux store。这里的dispatch
就是用来派发 action 的。异步操作(如
fetch
API)需要时间来完成。如果你在异步操作之前就返回一个 action,Redux 并不知道这个 action 应该在什么时候派发,它也无法等待异步操作的完成。使用dispatch
可以确保在异步操作(如 API 请求)完成之后再将最终的结果派发给 Redux store。dispatch
是 Redux 中派发 action 的函数,它通知 Redux store 有一个新的 action 被触发,Redux store 会根据这个 action 更新 state。
😲每一次fetch Data后,得到data之后有一个很好的习惯是console.log(data),来查看data的格式,方便运用它
(5)Redux Dev Tools
1、下载Dev ToolsRedux DevTools - Chrome 应用商店 (google.com)
2、npm i redux-devtools-extension
与视频一致,但此时已经有报错了(表明 redux-devtools-extension
版本与当前安装的 redux@5.0.1
版本不兼容。redux-devtools-extension@2.13.9
需要 redux
的版本是 ^3.1.0
或 ^4.0.0
,而我使用的是 redux@5.0.1
,导致了依赖冲突)
因此只能npm install redux-devtools-extension --legacy-peer-deps
来忽略 peerDependencies 的冲突,并继续安装
☑️在store.js
中修改
import { composeWithDevTools } from "redux-devtools-extension";
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)));
3、界面展示
⭕左边可以看到所有动作,并且可以直接jump到所有历史的状态,右边可以看到状态,下面的滑块栏也可以抵达历史动作的状态。
⭕点击该处可以手动dispatch action
(6)Redux Toolkit
✅compatible: 兼容的 ✅boilerplate样板代码 ✅mutate: 可变的
✅immutable: 不可变的 ✅Immer: Immer库(用于简化不可变数据处理的库)
1、creating store with RTK:
⭕npm i @reduxjs/toolkit
(同样版本冲突,按照上面的方法安装)
1 |
|
2、改变Slice
介于不想占用太多空间,这里仅给出AccountSlice的示例(以及不用RTK的版本)
1 |
|
3、使用RTK的优点🔆
代码简洁、减少样板代码:
RTK 中的
createSlice
自动生成 action types 和 action creators,因此减少了手动定义 action type 和 creator 的代码量。在不使用 RTK 的版本中,你需要手动编写大量的
switch-case
语句和 action creator,比如account/deposit
、account/withdraw
等。而在 RTK 中,createSlice
自动完成这些任务,简化了 reducer 和 action 的编写。
支持“可变”状态,简化状态更新逻辑:
- RTK 内部使用
Immer
, 允许你在 reducer 中编写像变异对象一样的代码,但实际上它并没有真的修改原始状态,而是生成了一个新的状态对象。这意味着:你在 reducer 中“修改”状态对象的字段时,Immer
会追踪这些变更,并创建一个新的状态对象,保持 Redux 的不可变性原则;如果你没有修改状态,Immer
会返回原始状态(不创建新的对象),从而优化性能。 - 你可以直接修改状态对象,而不需要手动创建状态的深拷贝,这大大简化了状态更新的代码。
- RTK 内部使用
内置异步处理和 middleware 支持:
RTK 提供了异步 action(如
createAsyncThunk
)的简化处理,内置了 thunk 中间件,简化了异步数据请求的管理。在你的代码中,deposit
异步处理逻辑依然需要用thunk
,但使用 RTK 时可以通过createAsyncThunk
或更好地支持异步流。你可以直接定义异步 action,不需要像传统 Redux 那样额外引入
redux-thunk
或手动创建异步操作。
自动生成 action creators 和 action types:
- RTK 自动生成 action creators,避免了手动定义 action type 的重复劳动。这不仅减少了错误的可能性,还使代码更具可维护性和一致性。
开发工具集成:
- RTK 与 Redux DevTools 以及其他 Redux 开发工具更好地集成,带有默认的优化配置,例如减少手动配置、提供性能优化等。
4、使用RTK的缺点⛈️
- 学习曲线:
- 尽管 RTK 提供了很多简化的工具,但对于那些已经熟悉传统 Redux 的开发者来说,理解和掌握 RTK 的 API 和设计模式可能需要一定的时间。比如像
createSlice
、createAsyncThunk
这些 API 对老用户来说可能需要一些时间适应。
- 尽管 RTK 提供了很多简化的工具,但对于那些已经熟悉传统 Redux 的开发者来说,理解和掌握 RTK 的 API 和设计模式可能需要一定的时间。比如像
- 隐藏的复杂性:
- 虽然 RTK 内部处理了不可变性和异步操作,但它隐藏了一些 Redux 的原理性代码(如状态的不可变性操作、手动定义
action
和reducer
的流程)。这对于希望深入了解 Redux 底层实现的开发者来说,可能减少了对框架的深度理解。 - 在代码中可以体现出来,使用RTK来自动创建action creator时默认只接收一个参数,因此需要用
prepare
函数来进行修改
- 虽然 RTK 内部处理了不可变性和异步操作,但它隐藏了一些 Redux 的原理性代码(如状态的不可变性操作、手动定义
- 与复杂应用的结合:
- 对于一些非常复杂或高度定制的应用,RTK 的封装可能不够灵活,开发者有时可能需要绕过 RTK 的一些默认行为,以适应应用的特定需求。在这些情况下,传统的 Redux 反而可能更灵活。
三、contextAPI
+useReducer
和Redux
的区别
1、中间件和插件支持
- Context API + useReducer:没有 Redux 那种丰富的中间件和插件生态系统,例如
redux-thunk
、redux-saga
之类的异步操作工具。通常需要手动处理复杂的异步逻辑。 - Redux:拥有大量中间件和开发工具(如 Redux DevTools和Middleware),可以很方便地调试和处理异步操作。
2、状态存储方式
- Context API + useReducer:没有全局的单一状态树。通过
useReducer
来管理本地状态,并通过Context API
提供状态共享的功能。不容易新增状态(要添加新的Provider和Reducer) - Redux:有一个全局的单一状态树(store),所有的应用状态都存储在这一个 store 中。这使得状态管理更加集中。创建新的状态slice较为方便
3、起步
- Context API + useReducer:由React直接提供,很容易建立
- Redux:需要提供插件支持,在初始建立的时候较为复杂
4、优化(具体见上一节)
- Context API + useReducer:优化较为复杂
- Redux:提供了很多开箱即用的优化