文章目录
1. 知识铺垫
1.1 理解JavaScript纯函数
函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
- 在 react 开发中纯函数是被多次提及的;
- 比如 react 中组件就被要求像是一个纯函数(为什么用像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求必须是一个纯函数;
- 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
纯函数的维基百科定义:
- 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
- 此函数在相同的输入值时,需产生相同的输出。
- 函数的输出与输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
当然上面的定义会过于的晦涩,简单总结一下:
- 确定的输入,一定会产生确定的输出;
- 函数在执行过程中,不能产生副作用;
1.2 副作用概念的理解
那么上面又有一个概念,叫做副作用
- 在计算机科学中,副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响
- 比如修改了全局变量,修改参数或者改变外部的存储;
纯函数在执行的过程中就是不能产生这样的副作用:副作用往往是产生bug的 “温床”。
1.3 纯函数的案例
来看一个对数组操作的两个函数:
- slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组;
- splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;
slice 就是一个纯函数,不会修改数组本身,而 splice 函数不是一个纯函数;
1.4 纯函数的作用和优势
为什么纯函数在函数式编程中非常重要
- 因为你可以安心的编写和安心的使用;
- 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;
- 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
React 中就要求我们无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:
在接下来学习 redux 中,reducer 也被要求是一个纯函数。
2. 为什么需要redux
JavaScript 开发的应用程序,已经变得越来越复杂了:
- JavaScript需要管理的状态越来越多,越来越复杂;
- 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;
管理不断变化的 state 是非常困难的:
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View 页面也有可能会引起状态的变化;
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
React 是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:
- 无论是组件定义自己的 state,还是组件之间的通信通过 props 进行传递;也包括通过 Context 进行数据之间的共享;
- React 主要负责帮助我们管理视图,state 如何维护最终还是我们自己来决定;
Redux 就是一个帮助我们管理State的容器:Redux 是 JavaScript 的状态容器,提供了可预测的状态管理;
Redux 除了和 React 一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
3. Redux 的核心概念
3.1 store
Redux 的核心理念非常简单。
- 比如我们有一个朋友列表需要管理:
- 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
- 比如页面的某处通过 products.push 的方式增加了一条数据;
- 比如另一个页面通过 products[0].age = 25 修改了一条数据;
整个应用程序错综复杂,当出现 bug 时,很难跟踪到底哪里发生的变化
const initialState ={
friends:[
{name:'foo',age:18},
{name:'coo',age:19},
{name:'hoo',age:20}
]
}
3.2 action
Redux 要求我们通过 action 来更新数据:
- 所有数据的变化,必须通过派发(dispatch)action来更新;
- action是一个普通的JavaScript对象,用来描述这次更新的 type 和 content;
比如下面就是几个更新 friends 的 action:
- 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可追踪、可预测的;
- 如:下面的 action 是固定的对象;真实应用中,我们会通过函数来定义,返回一个action;
const action1 = { type: 'ADD_FRIEND', info: {name:'licy',age:21} }
const action2 = { type: 'INC_AGE', index:0 }
const action3 = { type: 'CHANGE_NAME', playload: {index:0,newName:'lulu'} }
3.3 reducer
reducer 就是将 state 和 action 联系在一起的
- reducer 是一个纯函数;
- reducer 做的事情就是将传入的state和action结合起来生成一个新的state
function reducer(state = initialState, action) {
switch (action.type) {
case "ADD_FRIEND": // 添加一个朋友
return { ...state, friends: [...state.friend, action.info] }
case 'INC_AGE': // 给某个朋友的 age + 1
return {
...state, friends: state.friends.map((item, index) => {
if (index == action.index) {
return {...item,age:item.age+1}
}
return item;
})
}
case 'CHANGE_NAME':
return {
...state, friends: state.friends.map((item, index) => {
if (index === action.index) {
return {...item,name:action.newName}
}
return item;
})
}
default:
return state;
}
}
3.4 Redux 的三大原则
单一数据源
- 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:
- Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;
- 单一的数据源可以让整个应用程序的 state 变得方便维护、追踪、修改;
State是只读的
- 唯一修改 State 的方法一定是触发 action,不要试图在其他地方通过任何的方式来修改State:
- 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
- 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;
使用纯函数来执行修改
- 通过 reducer 将 旧state和 actions联系在一起,并且返回一个新的State:
- 随着应用程序的复杂度增加,我们可以将 reducer 拆分成多个小的 reducers,分别操作不同 state tree 的一部分;
- 但是所有的reducer都应该是纯函数,不能产生任何的副作用
3.5 Redux的使用过程
- 创建一个对象,作为我们要保存的初始状态:
- 创建 Store 来存储这个 state
- 创建 store 时必须创建 reducer;
- 我们可以通过
store.getState
来获取当前的 state;
- 通过 action 来修改 state
- 通过
dispatch
来派发 action; - 通常 action 中都会有 type 属性,也可以携带其他的数据;
- 通过
- 修改reducer中的处理代码
- 这里一定要记住,reducer是一个纯函数,不能直接修改state;
如果我们将所有的逻辑代码写到一起,那么当 redux 变得复杂时,代码就难以维护。
- 接下来,对代码进行拆分,将 store、reducer、action、constants 拆分成一个个文件。
- 创建
store/index.js
文件:创建 store - 创建
store/reducer.js
文件:定义 reducer - 创建
store/actionCreators.js
文件:将所有 action 函数统一定义于此文件 - 创建
store/constants.js
文件:管理 派发 action 时 标注(type)的名称
- 创建
【示例:在 App.jsx 文件中点击修改 name 和 age 按钮,能修改 store 中的数据】
1、store/index.js
文件
import { createStore } from "redux"
import {reducer} from './reducer'
// 创建 store
export const store = createStore(reducer);
// 订阅 subscribe; 在订阅了的地方,就能在 state 更新的之后 触发回调
const unsubscribe = store.subscribe(() => {
console.log("订阅数据发生了变化", store.getState());
});
// 调用 unsubscribe 即可取消订阅
// unsubscribe()
2、store/reducer.js
文件
import { CHANGE_AGE, CHANGE_NAME } from "./constants";
// 初始化的数据
const initialState = {
name: "zs",
age: 20,
};
// 定义 reducer 函数: 纯函数
// 接收 2 个 参数
// 参数一: store中 目前保存的 state
// 参数二:本次需要更新的action(dispatch传入的action)
// 返回值: 返回值会作为 store之后存储的 state
export function reducer(state = initialState, action) {
console.log("reducer:", state, action);
// 如果有新数据更新,那么返回一个 新的 state
switch (action.type) {
case CHANGE_NAME:
return { ...state, name: action.name };
case CHANGE_AGE:
return { ...state, age: action.age };
default:
return state;
}
// 没有新数据更新, 那么返回之前的 state
return state;
}
3、store/actionCreators.js
文件
import {CHANGE_AGE,CHANGE_NAME} from './constants'
export const changeNameAction = (name) => ({
type: CHANGE_NAME,
name,
});
export const changeAgeAction = (age) => ({
type: CHANGE_AGE,
age,
});
4、store/constants.js
文件
// 定义常量的文件
export const CHANGE_NAME = 'change_name'
export const CHANGE_AGE = 'change_age'
5、App.jsx
文件
import React, { PureComponent } from 'react'
import { store } from "./store";
import {changeAgeAction,changeNameAction} from "./store/actionCreator"
export default class App extends PureComponent {
constructor() {
super()
this.state ={name:store.getState().name}
}
updateName() {
// 修改Store中的数据:必须用 action
// store.dispatch({ type: 'change_name', name: 'foo' }); // 和下面一行的效果一样
store.dispatch(changeNameAction('foo'));
}
updateAge() {
const data = store.getState();
// store.dispatch({type:'change_age',age:data.age + 1})
store.dispatch(changeAgeAction(data.age + 1));
}
componentDidMount() {
// 在组件挂载的时候去订阅 store;store的数据一更新,就会通知所有订阅者更新
this.unsubscribe = store.subscribe(() => {
const state = store.getState()
this.setState({
name:state.name // 把需要的数据更新到组件自身上
})
})
};
componentWillUnmount() {
this.unsubscribe();// 记得在组件卸载时,取消订阅
}
render() {
return (
<div>
<button onClick={(e) => this.updateName()}>修改name</button>
<button onClick={(e) => this.updateAge()}>修改age</button>
</div>
);
}
}
但是如上所示,仅仅在一个组件中,使用 state 的 数据 ,就已经很繁琐了,更别说 组件可能需要很多 state 中的数据,甚至其他组件也要使用;下文中会用 react-redux库 简化流程
3.6 Redux 使用流程
认识一下 redux 在实际开发中的流程:
4. react-redux
4.1 react-redux基本使用
redux 和 react 没有直接的关系,你完全可以在 React, Angular, Ember, jQuery, or vanilla JavaScript 中使用 Redux。
尽管如此,redux 依然是和 React 库结合的更好,因为他们是通过state函数来描述界面的状态,Redux 可以发送状态的更新,让他们作出相应的更新。
redux 官方提供了 react-redux 的库,提供了 connect、Provider 等 API, 帮助我们连接 react 和 redux,实现的逻辑会更加的严谨和高效。
使用步骤:
一:下载 react-redux
库
npm i react-redux
二:过 react-redux 的 Provider
为后代组件注入 store
// 在 index.js 中,通过react-redux的Provider
// 使 Redux store 可用于任何需要访问 Redux store 的嵌套组件
import ReactDom from "react-dom/client";
import App from "./App.jsx";
import {Provider} from "react-redux"
import store from "./store/index.js";
const root = ReactDom.createRoot(document.querySelector("#root"));
root.render(
<Provider store={store}>
<App></App>
</Provider>
);
三:通过 react-redux 的 connect
连接组件,将 store 中的指定数据映射过去,作为 组件的 props 属性使用
// About.jsx 组件;该组件是App组件的后代组件
import React, { PureComponent } from 'react'
// 1. 引入 connect 函数
import { connect } from "react-redux"
import { addNumberAction,subNumberAction } from '../store/action'
export class About extends PureComponent {
changeNum(num,isAdd) {
if (isAdd) {
this.props.addNumber(num);
} else {
this.props.subNumber(num);
}
}
render() {
const { count } = this.props;
return (
<div>
<h2>About:{count}</h2>
<button onClick={(e) => this.changeNum(1, true)}>+1</button>
<button onClick={(e) => this.changeNum(1,false)}>-1</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
count: state.count, // 映射 store中的哪些数据
});
const mapDispatchToProps = (dispatch) => ({
// 映射 store 中的哪些 action ,然后 作为组件的 props (如:addNumber)去调用
addNumber(num) {
dispatch(addNumberAction(num));
},
subNumber(num) {
dispatch(subNumberAction(num));
},
});
// connect函数 的返回值是一个高阶函数
// connect 传入两个函数的作为参数
export default connect(mapStateToProps, mapDispatchToProps)(About);
4.2 redux&react-redux案例
4.2.1 结构和描述
案例描述:Left 和 Right 为App 的 子组件,Left 和 Right 共享 store 中 的 state (一个count);Left 组件能 使 count +1;Right 组件使 count -1;
结构如图:
下面 贴上各部分代码
4.2.2 store
store/constants.js
// 定义常量 ;action type 的 类型
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
store/actionCreator.js
import { INCREMENT, DECREMENT } from "./constants"
// 返回 action 的函数;记住 action 是个 对象
export const incrementAction = (num) => {
return {
type: INCREMENT,
num,
}
}
export const decrementAction = (num) => {
return {
type: DECREMENT,
num,
};
};
store/reducer.js
import * as actionTypes from "./constants"
// 创建一个对象,保存状态 state
const initialState = {
count: 100,
};
// 定义 reducer; 接收两个参数(state,action)
function reducer(state = initialState, action) {
const {INCREMENT,DECREMENT} = actionTypes
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + action.num };
case DECREMENT:
return { ...state, count: state.count - action.num };
default:
return state;
}
}
export default reducer
store/index.js
import { createStore } from "redux"
import reducer from "./reducer";
// 创建 redux store 来存放应用状态;并把 reducer传入
const store = createStore(reducer);
export default store;
4.2.3 pages
pages/Left.jsx
import React, { PureComponent } from 'react'
import {incrementAction} from "../store/actionCreators"
import { connect } from "react-redux"
class Left extends PureComponent {
add(num) {
this.props.onAddClick(num);
}
render() {
const { count } = this.props;
return (
<div>
<h2>Left: {count}</h2>
<button onClick={(e) => this.add(1)}>+1</button>
</div>
);
}
}
// 映射哪些 state 到 组件的 props 中
const mapStateToProps = state => {
return {
count:state.count
}
}
// 映射哪些 action 到 组件的 props中
const mapActionToProps = dispatch => {
return {
onAddClick: num => {
dispatch(incrementAction(num));
}
}
}
export default connect(mapStateToProps, mapActionToProps)(Left);
pages/Right.jsx
import React, { PureComponent } from 'react'
import { connect } from "react-redux";
import {decrementAction} from "../store/actionCreators"
class Right extends PureComponent {
sub(num) {
this.props.onSubClick(num)
}
render() {
const { count } = this.props;
return (
<div>
<h2>Right: { count }</h2>
<button onClick={(e) => this.sub(2)}>-2</button>
</div>
);
}
}
// 映射哪些 state 到 组件的 props 中
const mapStateToProps = state => {
return {
count:state.count
}
}
// 映射哪些 action 到 组件的 props中
const mapActionToProps = dispatch => {
return {
onSubClick: num => {
dispatch(decrementAction(num));
}
}
}
export default connect(mapStateToProps, mapActionToProps)(Right);
4.2.4 App.jsx
import React, { PureComponent } from 'react'
import Left from './pages/Left';
import Right from './pages/Right';
export default class App extends PureComponent {
render() {
return (
<div>
<h2>App</h2>
<Left></Left>
<Right></Right>
</div>
);
}
}
4.2.5 index.js
import ReactDom from "react-dom/client";
import App from "./App.jsx";
import { Provider } from "react-redux";
import store from "./store"
const root = ReactDom.createRoot(document.querySelector("#root"));
root.render(
<Provider store={ store }>
<App></App>
</Provider>
);
5. 异步数据处理
5.1 组件中的异步操作
在上面简单的案例中,redux 中保存的 count 是一个本地定义的数据
- 我们可以直接通过同步的操作来dispatch action,state就会被立即更新。
- 但是真实开发中,redux 中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到 redux 中。
网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:
// 当前位于 Category 组件
import React, { PureComponent } from 'react'
import axios from "axios"
import { connect } from 'react-redux';
import {getRecommend,getBanner} from "../store/action"
class Category extends PureComponent {
componentDidMount() {
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
// 获取了数据
const banner = res.data.data.banner.list;
const recommends = res.data.data.recommend.list;
// 更新 state;
this.props.changeBanner(banner);
this.props.changeRecommend(recommends);
})
}
render() {
console.log(this.props.banner); // 能拿到 banner
return (
<div>
<h2>Category</h2>
</div>
);
}
}
// 编写 映射的 state 和 action
const mapStateToProps = (state) => {
return {
banner: state.banner,
recommends: state.recommends,
};
}
const mapActionToProps = (dispatch) => {
return {
changeBanner(banner) {
dispatch(getBanner(banner));
},
changeRecommend(recommend) {
dispatch(getRecommend(recommend));
}
}
}
export default connect(mapStateToProps, mapActionToProps)(Category);
// 当前位于 action.js 文件
export const getBanner = (banner) => ({
type: actionTypes.GET_BANNER,
banner
})
export const getRecommend = (recommend) => ({
type: actionTypes.GET_RECOMMEND,
recommend,
});
省略其他文件代码
5.2 redux 中异步操作
上面的代码有一个缺陷:
- 必须将网络请求的异步代码放到组件的生命周期中来完成;
- 事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给 redux 来管理;
但是在 redux 中如何进行异步的操作呢?
- 答案就是使用中间件(Middleware);
- 学习过 Express 或 Koa 框架的同学对中间件的概念一定不陌生;
- 在这类框架中,Middleware 可以帮助我们在请求和响应之间嵌入一些操作的代码,比如 cookie 解析、日志记录、文件压缩等操作;
5.2.1 理解中间件
redux 也引入了中间件(Middleware)的概念:
- 这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;
- 比如日志记录、调用异步接口、添加代码调试功能等等;
我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:
- 这里官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk 库;
redux-thunk 是如何做到让我们可以发送异步的请求呢?
- 我们知道,默认情况下的 dispatch(action),action需要是一个JavaScript的对象;
- redux-thunk 可以 dispatch(action函数),即action可以是一个函数;
- 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
- dispatch 函数用于我们之后再次派发 action;
- getState 函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;
5.2.2 使用 redux-thunk
1、安装redux-thunk
npm i redux-thunk
2、在创建 store 时传入应用了 middleware 的 enhance
函数
- 通过
applyMiddleware
来结合多个 Middleware, 返回一个enhancer; - 将 enhancer 作为第二个参数传入到 createStore 中;
3、定义返回一个函数的action:
- 注意:这里不是返回一个对象了,而是一个函数;
- 该函数在 dispatch 之后会被执行;
【示例代码】
// store -> index.js
// 增强 store
import { createStore,applyMiddleware } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer";
// 这里直接 为 store 通过 applyMiddleware 增加了 thunk 中间件
const store = createStore(reducer,applyMiddleware(thunk));
export default store;
// 在 store->action.js
// 定义 获取网络请求 action
import axios from "axios";
export const getBanner = (banner) => ({
type: actionTypes.GET_BANNER,
banner
})
export const fetchDataAction = () => {
// 如果是一个普通的action,那么这里需要返回 action 对象
// 如果返回的是一个函数,那么 redux是不支持的,需要使用 redux-thunk 库
return function (dispatch, getState) {
// 异步操作: 网络请求
axios.get("http://123.207.32.32:8000/home/multidata").then((res) => {
const banner = res.data.data.banner.list;
dispatch(getBanner(banner));
});
};
}
// 在 组件中,直接调用一个函数,即可完成 异步请求数据到 store 中
import {fetchDataAction} from "../store/action"
class Category extends PureComponent {
componentDidMount() {
this.props.fetchData();
}
render() {
return (
<div>
<h2>Category:{this.props.count}</h2>
</div>
);
}
}
const mapActionToProps = (dispatch) => {
return {
fetchData() {
dispatch(fetchDataAction());
}
};
};
export default connect(null, mapActionToProps)(Category);
6. 添加 react 开发工具
redux 可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?
- redux 官网为我们提供了redux-devtools 的工具;
- 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;
安装该工具需要两步:
- 第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索 Redux DevTools 即可);
- 第二步:在 redux 中继承 devtools 的中间件;
import { createStore,applyMiddleware,compose } from "redux";
import thunk from "redux-thunk";
import reducer from "./reducer";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
再推荐一个 React Developer Tools 插件,可以检查 React components、编辑 props 和 state
7. Reducer代码拆分
假设 这个 reducer 既有处理 count 的代码,又有处理 home(banner数据) 页面的数据;
- 后续 count 相关的状态或 home 相关的状态会进一步变得更加复杂;
- 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
- 如果将所有的状态都放到一个 reducer 中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
因此,我们可以对 reducer 进行拆分:
- 我们先抽取一个对counter处理的 reducer;
- 再抽取一个对 home 处理的 reducer;
- 将它们合并起来;
这里仅展示 count 模块的四个文件代码
1、store -> count>constants.js
export const ADD_NUMBER = "add_number";
export const SUB_NUMBER = "sub_number";
2、store->count->actionCreators.js
import * as actionTypes from "./constants";
export const addNumberAction = (num) => ({
type: actionTypes.ADD_NUMBER, num
});
export const subNumberAction = (num) => ({
type: actionTypes.SUB_NUMBER,
num,
});
3、store->count->reducer.js
import * as actionTypes from "./constants"
const initialState = {
count:100
}
function reducer(state = initialState,action) {
switch (action.type) {
case actionTypes.ADD_NUMBER:
return { ...state, count: state.count + action.num };
case actionTypes.SUB_NUMBER:
return { ...state, count: state.count - action.num };
default:
return state;
}
}
export default reduce
4、store->count->index.js
import reducer from "./reducer"
export default reducer
export * from "./actionCreators"
5、store -> index.js
import { createStore,applyMiddleware,compose,combineReducers } from "redux";
import thunk from "redux-thunk";
import countReducer from "./count"
import homeReducer from "./home"
// 将两个 reducer 合并在一起
const reducer = combineReducers({
count: countReducer,
home: homeReducer,
});
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
在组件中使用数据时,和之前的区别是,需要多一层指定,来告诉 store ,你需要的是哪个模块的状态 (例如:store.getState().count.count 表示获取 count 模块中 count 数据)
由上述可见,redux 给我们提供了一个 combineReducers 函数可以方便的让我们对多个 reducer 进行合并:
8. Redux Toolkit
8.1 认识Redux Toolkit
Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。
- 在前面学习 Redux 的时候应该已经发现,redux 的编写逻辑过于的繁琐和麻烦。
- 并且代码通常分拆在多个文件中,如store文件,常量文件,reducer文件,action文件(虽然也可以放到一个文件中,但是代码量过多时,不利于管理);
- Redux Toolkit 旨在成为编写 Redux 逻辑的标准方式,从而解决上面提到的问题;-- 很多地方为了称呼方便,也将之称为“RTK”;
安装 Redux Toolkit:
npm install @reduxjs/toolkit react-redux
Redux Toolkit 的核心 API 主要是如下几个:
configureStore
:包装 createStore 以提供简化的配置选项和良好的默认值。它可以自动组合你的 slice reducer,添加你提供的任何 Redux 中间件,redux-thunk 默认包含,并启用 Redux DevTools Extension。createSlice
:接收 reducer 函数的对象、切片名称和初始状态值,并自动生成切片reducer,并带有相应的 actions。createAsyncThunk
: 接收一个动作类型字符串和一个返回承诺的函数,并生成一个 pending/fulfilled/rejected 基于该承诺分派动作类型的 thunk
8.2 创建 reducer
通过 createSlice 创建一个slice。
import { createSlice } from "@reduxjs/toolkit";
const countSlice = createSlice({
name: 'count',
initialState: {
count:100,
},
reducers: {
addCount(state,{payload}) {
state.count = state.count + payload
},
subCount(state, { payload }) {
state.count = state.count - payload;
}
}
})
export const {addCount,subCount} = countSlice.actions
export default countSlice.reducer
createSlice 主要包含如下几个参数:
1、name:用户标记 slice 的名词
- 在之后的 redux-devtool 中会显示对应的名词;
2、initialState:第一次初始化时的值;
3、reducers:相当于之前的 reducer 函数
- 对象类型,并且可以添加很多的函数;
- 函数类似于 redux 原来 reducer 中的一个case语句;
- 函数的参数:
- 参数一:state
- 参数二:调用这个 action 时,传递的 action 参数;
4、createSlice 返回值是一个对象,包含所有的 actions;
8.3 store 的创建
configureStore 用于创建 store 对象,常见参数如下:
- reducer:将 slice 中的 reducer可以组成一个对象传入此处;
- middleware:可以使用参数,传入其他的中间件(自行了解);
- devTools:是否配置 devTools工具,默认为 true;
import { configureStore } from "@reduxjs/toolkit";
import countReducer from "./features/count"
import homeReducer from "./features/home"
const store = configureStore({
reducer: {
count: countReducer,
home: homeReducer,
},
});
export default store
8.4 Redux Toolkit的异步操作
在之前的开发中,我们通过 redux-thunk 中间件让 dispatch 中可以进行异步操作。
Redux Toolkit默认已经给我们集成了 Thunk 相关的功能:createAsyncThunk
当 createAsyncThunk 创建出来的 action 被 dispatch 时,会存在三种状态:
- pending:action被发出,但是还没有最终的结果;
- fulfilled:获取到最终的结果(有返回值的结果);
- rejected:执行过程中有错误或者抛出了异常;
我们可以在 createSlice 的 entraReducer 中监听这些结果:
【示例:home 的 数据】
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchHomeDataAction = createAsyncThunk("fetctHomeData", async (extraInfo,store) => {
// 发送网络请求,获取数据
const res = await axios.get("http://123.207.32.32:8000/home/multidata");
// 这里有两种方式可以改变 store 中 的 数据
// 1.一是直接 通过 store 的 dispatch去修改
// 2.二是通过 return 出去的数据在 extraReducers中,去修改
// const banners = res.data.data.banner.list;
// store.dispatch(changeBanners(banners));
return res.data;
});
const homeSlice = createSlice({
name: "home",
initialState: {
banners: [],
},
reducers: {
changeBanners(state, { payload }) {
state.banners = payload;
},
},
extraReducers: {
[fetchHomeDataAction.pending](state, action) {console.log('pending')},
[fetchHomeDataAction.fulfilled](state, action) {
const banners = action.payload.data.banner.list;
state.banners = banners;
},
[fetchHomeDataAction.rejected](state, action) {console.log('rejected')},
},
});
export const { changeBanners } = homeSlice.actions;
export default homeSlice.reducer;
extraReducer的另外一种写法(了解即可)
extraReducers: (builder) => {
builder
.addCase(fetchHomeDataAction.pending, (state) => {
console.log("fetchHomeDataAction pending");
})
.addCase(fetchHomeDataAction.fulfilled, (state,{payload}) => {
state.banners = payload.data.banner.list;
})
.addCase(fetchHomeDataAction.rejected, (state) => {
console.log("fetchHomeDataAction rejected");
});
}
【补充Home组件】
import React, { PureComponent } from 'react'
import { connect } from "react-redux";
import { subCount } from "../store/features/count"
// import store from '../store';
import {fetchHomeDataAction} from "../store/features/home"
class Home extends PureComponent {
componentDidMount() {
this.props.fetchHomeData();
}
subClick(num) {
this.props.subNumber(num);
// store.dispatch(subCount(num));
// store.dispatch(fetchHomeDataAction());
}
render() {
const { count,banners } = this.props;
return (
<div>
<h2>Home中的count:{count}</h2>
<ul>
{banners.map((item, index) => {
return <li key={index}>{item.title}</li>;
})}
</ul>
<button onClick={(e) => this.subClick(2)}>-2</button>
<hr />
</div>
);
}
}
const mapStateToProps = (state) => {
return {
count: state.count.count,
banners: state.home.banners,
};
}
const mapDispatchToProps = (dispatch) => {
return {
subNumber(num) {
dispatch(subCount(num));
},
fetchHomeData() {
dispatch(fetchHomeDataAction());
}
};
}
export default connect(mapStateToProps,mapDispatchToProps)(Home);
8.5 RTK的数据不可变性
在 React 开发中,我们总是会强调数据的不可变性:
- 无论是类组件中的 state,还是 redux 中管理的 state;
- 事实上在整个JavaScript编码过程中,数据的不可变性都是非常重要的;
所以在前面我们经常会进行浅拷贝来完成某些操作,但是浅拷贝事实上也是存在问题的:
- 比如过大的对象,进行浅拷贝也会造成性能的浪费;
- 比如浅拷贝后的对象,在深层改变时,依然会对之前的对象产生影响;
事实上 Redux Toolkit 底层使用了 immerjs 的一个库来保证数据的不可变性。
为了节约内存,又出现了一个新的算法:Persistent Data Structure(持久化数据结构或一致性数据结构);
- 用一种数据结构来保存数据;
- 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费;
8.6 redux toolkit 案例
8.6.1 结构和描述
案例描述:在Home 组件 App 组件,Profile 组件 共享 state 的count ,Home 组件 的 banner 数据 是异步获取的;
结构如图:
8.6.2 store
store/features/count.js
import { createSlice } from "@reduxjs/toolkit";
const countSlice = createSlice({
name: 'count',
initialState: {
count:100,
},
reducers: {
addCount(state,{payload}) {
state.count = state.count + payload
},
subCount(state, { payload }) {
state.count = state.count - payload;
}
}
})
export const {addCount,subCount} = countSlice.actions
export default countSlice.reducer
store/features/home.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchHomeDataAction = createAsyncThunk("fetctHomeData", async (extraInfo,store) => {
// 发送网络请求,获取数据
const res = await axios.get("http://123.207.32.32:8000/home/multidata");
// 这里有两种方式可以改变 store 中 的 数据
// 1.一是直接 通过 store 的 dispatch去修改
// 2.二是通过 return 出去的数据在 extraReducers中,去修改
// const banners = res.data.data.banner.list;
// store.dispatch(changeBanners(banners));
return res.data;
});
const homeSlice = createSlice({
name: "home",
initialState: {
banners: [],
},
reducers: {
changeBanners(state, { payload }) {
state.banners = payload;
},
},
extraReducers: {
// [fetchHomeDataAction.pending](state, action) {console.log('pending')},
[fetchHomeDataAction.fulfilled](state, action) {
const banners = action.payload.data.banner.list;
state.banners = banners;
},
// [fetchHomeDataAction.rejected](state, action) {console.log('rejected')},
},
// extraReducers: (builder) => {
// builder
// .addCase(fetchHomeDataAction.pending, (state) => {
// console.log("fetchHomeDataAction pending");
// })
// .addCase(fetchHomeDataAction.fulfilled, (state,{payload}) => {
// state.banners = payload.data.banner.list;
// })
// .addCase(fetchHomeDataAction.rejected, (state) => {
// console.log("fetchHomeDataAction rejected");
// });
// }
});
export const { changeBanners } = homeSlice.actions;
export default homeSlice.reducer;
store/index.js
import { configureStore } from "@reduxjs/toolkit";
import countReducer from "./features/count"
import homeReducer from "./features/home"
const store = configureStore({
reducer: {
count: countReducer,
home: homeReducer,
},
});
export default store
8.6.3 pages
pages/Home.jsx
import React, { PureComponent } from 'react'
import { connect } from "react-redux";
import { subCount } from "../store/features/count"
// import store from '../store';
import {fetchHomeDataAction} from "../store/features/home"
class Home extends PureComponent {
componentDidMount() {
this.props.fetchHomeData();
}
subClick(num) {
this.props.subNumber(num);
// store.dispatch(subCount(num));
// store.dispatch(fetchHomeDataAction());
}
render() {
const { count,banners } = this.props;
return (
<div>
<h2>Home中的count:{count}</h2>
<ul>
{banners.map((item, index) => {
return <li key={index}>{item.title}</li>;
})}
</ul>
<button onClick={(e) => this.subClick(2)}>-2</button>
<hr />
</div>
);
}
}
const mapStateToProps = (state) => {
return {
count: state.count.count,
banners: state.home.banners,
};
}
const mapDispatchToProps = (dispatch) => {
return {
subNumber(num) {
dispatch(subCount(num));
},
fetchHomeData() {
dispatch(fetchHomeDataAction());
}
};
}
export default connect(mapStateToProps,mapDispatchToProps)(Home);
pages/Profile.jsx
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import store from '../store'
import { addCount } from "../store/features/count"
class Profile extends PureComponent {
addCount(num) {
// this.props.addClick(num);
store.dispatch(addCount(num));
}
render() {
const {count} = this.props
return (
<div>
<h2>Profile中的count{count}</h2>
<button onClick={e=>this.addCount(8)}>+8</button>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
count: state.count.count,
}
}
const mapDispatchToProps = (dispatch) => {
return {
// addClick(num) {
// dispatch(addCount(num));
// },
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
8.6.4 App.jsx
import React, { PureComponent } from 'react'
import Home from './pages/Home';
import Profile from './pages/Profile';
import { connect } from 'react-redux';
class App extends PureComponent {
render() {
const {count} = this.props
return (
<div>
<h2>App: count:{count}</h2>
<hr />
<Home></Home>
<Profile></Profile>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
count:state.count.count
}
}
export default connect(mapStateToProps)(App);
8.6.5 index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import store from './store';
import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={ store }>
<App />
</Provider>
);
9. React 状态管理方案
前面学习了 Redux 用来管理我们的应用状态,并且非常好用
目前我们已经主要学习了三种状态管理方式:
- 方式一:组件中自己的state管理;
- 方式二:Context 数据的共享状态;
- 方式三:Redux 管理应用状态;
在开发中如何选择
- 首先,这个没有一个标准的答案;
- 某些用户,选择将所有的状态放到 redux 中进行管理,因为这样方便追踪和共享;
- 有些用户,选择将某些组件自己的状态放到组件内部进行管理;
- 有些用户,将类似于主题、用户信息等数据放到Context中进行共享和管理;
- 做一个开发者,到底选择怎样的状态管理方式,也是工作之一,要找到一个的平衡方式
目前项目中推荐采用的 state 管理方案:
- UI 相关的组件内部可以维护的状态,在组件内部自己来维护;
- 大部分需要共享的状态,都交给 redux 来管理和维护;
- 从服务器请求的数据(包括请求的操作),交给 redux 来维护;