redux不好用?万字细究redux项目实操
大家好,我是张添财。每次提及redux可能会有很多小伙伴头疼,究其原因redux的使用链路比较长。并且由于其灵活性较强,尤其进行模块化拆分后,我们看着那一堆的文件免不了要瑟瑟发抖。本文旨在帮助大家从头到尾深入探究redux,并且引入书店借书这个实际场景与加深大家的记忆。好了,嘿喂购,咱这就开始!
一、react 组件通信
众所周知,react的思想可以用一个公式来概括:UI=Render(state) 从这我们就不难看出:在开发react业务时,实际上就是在操作react的state状态去进行rerender来改变UI视图。那既然这样,我们如何管理state状态,如何操作state来进行组件通信就显得尤为重要了。 这也就引出了我们接下来要介绍的业务中组件通信常用的三种方式:
1.1 props: 组件间流式传值
通常情况下,如果我们的state通信只涉及到两三个层级,我们常常用props传值的方式来进行传递state数据从而实现组件间的数据通信。如下图:
const ChildFirstComp=(props)=>{
return (<div>
这是从ChildSecondComp传进来的值:{props.innerVal}
</div>)
}
const ChildSecondComp=(props)=>{
return (<div>
这是从ParentComp传进来的值:{props.value}
<ChildFirstComp innerVal={props.value}/>
</div>)
}
const ParentComp=(props)=>{
const testVal='这是在父组件声明的值'
return (<div>
这是ParentComp组件
<ChildSecondComp value={testVal}/>
</div>)
}
从上面我们可以看出,如果组件的嵌套层级不深的情况下,props传值还是很方便的。但是一旦组件层级非常深,我们再采用props传值的这种方式就很容易陷入流式传递的陷阱。也即我们的state就像水流一样,从顶层组件一层一层往内层传递,这显然加重了我们的开发负担,通信链路过长。所以,总的来说,简单的父子传值以这种方式通信没问题,但复杂层级嵌套还是得另行考虑通信方式!
1.2 Context: 局部共享数据
上文说道,pros传值不适合嵌套层级过深的组件通信。如果我们想在嵌套层级过深的组件之间通信,且兄弟组件之间也进行相关state传递时,我们可以利用Context的方式来进行数据共享。
Context使用步骤:
- 调用 React.createContext() 创建一个Context对象 。Context对象中包含 Provider(提供数据) 和 Consumer(消费数据) 两个组件
import React from 'react'
export const GrandContext=React.createContext()
- 在顶层组件中引入grandContext中Provider组件进行提供数据,并设置value属性。注意,Provider组件里的value属性值就是内部要共享的数据值:
import React from 'react'
import {GrandContext} from './grandContext'
...
export default function GrandFDemo() {
return (
<Provider value={{grandVal:'这是Provider提供的数据'}}>
<div>
TestDemo
<FaDemo></FaDemo>
</div>
</Provider>
)
}
...
- 在顶层组件内部包裹的组件中,哪一层想要接收数据,这一层就用Consumer包裹此组件,若要使用共享的value值在在回调函数中使用第一个参数即可,如下面代码所示:data参数就表示Provider提供的共享数据
<GrandContext.Consumer>
{
data => {
return (
<div>SonDemo:{data.grandVal}</div>
)
}
}
</GrandContext.Consumer>
这是完整代码:
import React from 'react'
import {GrandContext} from './grandContext'
const SonDemo=() => {
return (
<GrandContext.Consumer>
{
data => {
return (
<div>SonDemo:{data.grandVal}</div>
)
}
}
</GrandContext.Consumer>
)
}
const FaDemo=() => {
return (
<div>
FaDemo
<SonDemo></SonDemo>
</div>
)
}
export default function GrandFDemo() {
return (
<GrandContext.Provider value={{grandVal:'这是Provider提供的数据'}}>
<div>
GrandFDemo
<FaDemo></FaDemo>
</div>
</GrandContext.Provider>
)
}
实际业务里,我们常用useContext钩子来代替上面这种Consumer组件的写法:
import React , { useContext } from 'react'
import {GrandContext} from './grandContext'
...
const SonDemo=() => {
const proVal=useContext(GrandContext)
return (
<div>
SonDemo:{proVal.grandVal}
</div>
)
}
...
至此我们也就基本掌握了Context使用。各位小伙伴看完Context顿时感觉Context大法好呀,这么一来我的共享数据直接在顶层维护就好了,其他组件哪里想用哪里调useContext取值就行了,美滋滋。但是该说不说,Context 还是有几个缺点:
-
只有在Provider包裹内的组件才可以使用共享的value值,也即value值的共享范围还不够宽泛。
-
我们Provider共享出去的value只要有更新,所有消费该Context共享数据的组件都会触发rerender。也就是说,如果我们的context的数据里有多个key,只有其中一个key(咱称为key1)会频繁更新,其他key值都比较稳定。如果key1值发生变化,即使子孙组件只使用了其他的key值而没有消费key1,它依然会频繁渲染,这就很容易会导致性能问题。(优化手段一般是拆分context或者缓存UI组件)
现在我们再引申这么一个场景需求:state数据需要广泛应用在项目里各个组件,并且这些组件没有很规律的层级关系。我还需要这个state能被改变,state变化的同时也要引起视图变化。并且,除此之外,我还要求能严格控制组件的rerender。
看完这个需求,我们常规的props传值和Context似乎就有点不够看了。那怎么办呢?关门,放redux!
1.3 Redux: 状态管理中心
我们在开始介绍redux之前,需要清楚一点:redux 并非react专属,它也可以在jQ、Angular 里使用。只是redux和React结合比较好,因为react的原则是通过state来描述界面的状态,而Redux可以派发状态的更新,让react作出响应。
redux是一个状态管理库,它的使用流程其实很明晰:通过dispatch派发action来修改store里的state数据,而连接state与action的就是我们使用时重点去维护的reducer。redux可以帮助我们实现数据的全局共享,我们引入redux之后,就达到实现无视组件层级哪里需要哪里用的目的。并且redux内部集成了相关api来帮助我们去触发视图的更新,也就是说,我们利用redux就可以做到“按需render”,这也就解决了Context的频繁render的情况。
说完了概念,咱们再来说明几条使用redux时遵循的原则:
-
单一数据源store:redux建议我们只创建一个store来进行数据管理,这样可以方便我们维护全局共享数据state。
-
共享数据state只读:redux要求我们只能用dispatch派发action的方式来修改state,不能在业务里直接操作state进行修改赋值。
-
使用纯函数reducer来操作action和state:redux要求我们使用的reducer都应该是纯函数,不能产生任何的副作用;
好了,到此我们初遇redux的阶段就结束了,下面我们就来好好盘盘redux里相关的API以及一些和redux有关的第三方库吧。
二、换个角度看redux
上一part的结尾部分,我们初步介绍了redux。但相信很多小伙伴心里免不了吐槽:“这介绍确实太初步了,说了点东西,但感觉又没说。” 别急别急,咱接下来就是本文的重头戏:里里外外细说redux。
我个人一直觉得知识应该是鲜活的,如果只堆概念不仅有些无聊,而且各位小伙伴的吸收效果也不见得有多好。还会出现我看了,但转头就忘了的现象。添财今天换个方式来同各位一起探究redux,咱直接把redux搬到现实生活中,用去书店借书来做个类比。让各位好好了解redux整个处理流程!
2.1 概念类比,redux照进现实
各位彦祖、胡歌们都去过书店,借书还书那肯定也是常事(当然去图书馆蹭空调的是谁我不说....)好了,那现在咱把redux揪到我们生活中来。把书店和redux做一层映射:
① 我们把redux的store比作新华书店(新华书店打钱!!);
② 把reducer比作图书管理员;
③ 把state比作新华书店里的书;
④ 业务里要用到state数据的地方就是我们这些去借书的彦祖们;
⑤ 而我们想要借哪本书,借几本的这个行为就是action;
⑥ 我们借书要用的书店借记卡就是redux里的dispatch。
好了,映射做完了,咱们此时来回忆一下去新华书店借书的流程:我现在想要看书了于是就跑去书店找图书管理员,找到后,我拿着书店借记卡说:咱现在要借一本《明朝那些事》,图书管理员听到后一顿骚操作就把书借给我了。现在书店里能借出去的书少了一本,咱现在手里就拿着《明朝那些事》开始看。
借书流程回忆完了,下一步咱们来用redux里的相关术语来复现一下这个借书的流程:
- 首先,新华书店(store)全部的书是redux里的state数据;
- 我现在想要看书的这个需求翻译一下就是我们业务里需要用到store里的state;
- 咱拿借记卡跑去书店里问管理员借了一本书,这个行为翻译成业务语言就是:业务里需要用到state数据,咱在业务中用dispatch派发了action操作,reducer接收到这个action之后根据我们的action来处理相关的state数据;
- 借到了《明朝...》则是 我们在业务开发中拿到了所需的state数据;
- 书店里能借出去的书少了一本 则表示reducer更新了当前的state数据。
2.2 关键点总结
好了,上面我们已经把redux和我们书店借书的流程完美的糅合在一起了,下面咱们来提取一下关键点:
① 我想要借书,就必须要去书店。那我们要用redux,就必须得有store;
② 书店将图书借给很多来借书的人,那store也把state数据共享给我们项目里的各个业务逻辑;
③ 书店得有图书管理员去管理这些书,那store里也得有管理state的管理者reducer;
④ 我去书店借书必须向图书管理员说我要借什么书,这样管理员才知道去拿哪本书。那业务使用操作state时也需要action来说明我们要怎么操作state,这样reducer才能进一步去识别;
⑤ 我们借书得有借记卡,总不能拿添财书店的卡借新华书店的书。那相应的,业务里我们要触发action操作也只能通过 dispatch 来进行,否则,reducer不认。
⑥ 书店里的图书经过管理员的借记操作完成了可借图书数量的更新。store也通过reducer完成了对全局共享数据state的更新。
唠了这么多,咱的目的就是为了让大家对操作redux的流程大致有个了解,如果按照传统那种只堆概念,我个人感觉是有点生硬的。这种介绍方式应该会让各位印象深刻些。
到这,相信大家也总结出了关键词:存放数据的store;管理数据的reducer;能让图书管理员肯拿书出借的借记卡dispatch;能让管理员知道要操作哪本书的action。至此,咱就要开启下一part,对这些关键词进一步探讨。
三、redux 细究
我们在项目里使用redux拢共分四步:① 创建书店 store 塞满图书state 、② 招聘图书管理员reducer、③ 维护下我们借书的操作actions、④ 在业务逻辑里去操作 state。好了,工欲善其事必先利其器,咱想用redux,那不得先下载这个三方库嘛:
yarn add redux
下载好之后,咱就开始从零到一创建”新华书店“吧(手头狗头)
3.1 Store :应有尽有的书店
在redux里,store就是我们的书店。它就是state的数据管理中心。我们整个项目最好只有一个 Store。在我们开发时,需要通过 legacy_createStore方法来创建,此方法的返回值就是我们所需的store对象。store对象中包含getState、dispatch、subscribe等api以供我们来管理state数据。在调用legacy_createStore方法时,我们必须传入reducer作为参数。
legacy_createStore 参数含义如下所示:
① 参数一(必填):项目里的reducers,联系项目里state与action,state也通过reducers完成数据的更新。
② 参数二(非必填):设置state的初始值。但是此处设置的值会覆盖掉在reducer里设置的初始值。
③ 参数三(非必填):redux的增强器,我们chrome的redux插件、支持redux进行异步调用的中间件都是在此配置。
为了方便我们开发时在浏览器中查看redux里的状态值,可以安装redux-devtools-extension
插件来增强redux功能。将此插件的 composeWithDevTools API传到legacy_createStore里即可生效( 记得先在浏览器里下载拓展啊)
yarn add redux-devtools-extension
import { legacy_createStore as createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
const rootReducer=(state={val:'这里会被覆盖掉',counter:0},action)=>{return state}
// 创建 Store 实例
const store = createStore(
// 参数一:根 reducer
rootReducer,
// 参数二:初始化时要加载的状态
{
counter: 11
},
// 参数三:增强器,加中间件
composeWithDevTools()
)
// 导出 Store 实例,
export default store
至此,我们的store就创建好了,下面我们就来测试下store有没有生效,我们可以调用store对象上的 getState 方法来获取当前store里的state数据。
代码如下:
import React from 'react'
import store from '@/store'
export default function GrandFDemo() {
return (
<div>
这是展示store里的结果:{store.getState().counter}
</div>
)
}
好了,我们的书店就创建好了,store部分就暂时告一段落,书店怎么去响应我们借阅图书的操作员,我们又如何具体操作我们的state数据则放到下文细说。
3.2、reducer : 尽职尽责的管理员
我们刚刚已经把新华书店建立起来了,那下一步必须得招聘个管理员呐。不出意外,这一part我们来介绍下我们的图书管理员:reducer。
首先我们的管理员reducer就是一个函数,此函数可以接收两个参数:state 和 action。state是我们当前的全局共享数据,action则是一个对象其中则包含我们想要如何去修改state的相关属性。
这也很容易理解,毕竟图书管理员就是要知道我们想借哪本书,想要借几本。对应到这里就是reducer要知道我们要进行操作的action,再根据我们的action去对state进行相应的处理。
reducer会接收这两个参数并返回一个新的state,这个更新后的state值就是最新的全局共享数据值。 除此之外,我们还需要特别注意的一点:reducer得是一个纯函数,他不能对外部产生副作用!
3.2.1 补充知识:纯函数
纯函数: 一个函数的返回结果只依赖于它传进来的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
纯函数的条件:
① 返回结果只依赖传进来的参数(参数怎么来的怎么走,不改变传进来的参数);
② 在执行过程中没有副作用。
由于 Reducer 是纯函数,同样的 state 必定得到同样的 view,所以 Reducer 函数内不能直接对原 state进行赋值操作 必须返回一个全新的对象。
好了,到这里咱就来做一个较具官方味道的发言性总结:各位同好们,所谓 reducer 就是 redux 根据派发的action来具体操作state数据的一个纯函数,此函数内部一般会用switch或者if逻辑来对不同的action类型进行分类处理,从而返回一个更新后的state数据。
下面我们就该进入到代码阶段了,光说不练假把式,话不多说,show me the code!
3.2.2 代码实操
我们还是沿用store部分的代码,此处咱就是来拓展一下reducer,主要是往reducer塞我们的state处理逻辑,让reducer可以根据不同的action类型来进行相应的操作返回。
代码如下:
import { legacy_createStore as createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
//注意,这里设置的初始值会被createStore处传入的初始值覆盖
const initState={counter:666}
const rootReducer=(state=initState,action)=>{
switch (action.type) {
case "TEST_ONE":
return {...state, counter:111};
case "TEST_TWO":
return {...state, counter:222};
case "TEST_THREE":
return {...state, counter:888}
default:
return state;
}
}
// 创建 Store 实例
const store = createStore(
// 参数一:根 reducer
rootReducer,
// 参数三:增强器,加中间件
composeWithDevTools()
)
// 导出 Store 实例,
export default store
我们的书店已经创建,管理员也到位了,接下来就该咱出场去借书了。
3.3 dispatch和action:借记卡和借书操作
一卡在手,图书我有!前面铺垫了那么多,现在终于可以借书了。借书之前咱还是得想想要借什么书......思索一会,咱想借《明朝那些事儿》,于是有了下面这段代码:
const action={
type:'MING_DYNASTY',
num:1
}
这个action就表示我想借明朝,并且只借一本。紧接着来了一个问题,咱要借书,那必须得有个凭证呐。没凭证书店也不给借呀,不然要是借了不还该咋办?诶,这里我们的dispatch就闪亮登场了✨!

正如我们需要借记卡去借书管理员才会处理,我们在业务里必须通过dispatch派发action后reducer才会进行相应处理。好了,终于把正主引出来了,接下来我们就来探究下action和dispatch的使用吧。
3.3.1 初识 action
action从类型上来说就是一个对象,此对象内部包含我们的操作类型type、state相关数据。之所以要包含操作类型type 是因为我们的action肯定不止有一个,既然这样我们就用type去区分不同action操作。区分了action操作,我们当然要知道每个action都要带来什么样的值来让reducer处理更新,这样我们就用payload来传相关数据。所以一般的,我们在真实开发中都会这么设置action对象:
const action={
type:'这里的值要和reducer中的type值对应',
payload:{ 这里的值就是参与到state值更新的相关数据 }
}
// 当然,我们在业务里也经常使用函数来动态生成action
const generateAction=(type,payload)=>{
return {
type,
payload
}
}
action设计好了,下面该轮到dispatch了。在上面我们也说明了要想action能被reducer处理,我们在业务里必须通过dispatch来进行派发。一旦业务里使用dispatch派发action后,我们的reducer就会根据action去处理state并且返回更新后的state值。
3.3.2 初遇 dispatch
dispatch是我们通过createStore创建出来的store对象里的api,其作用就是派发action给reducer。需要注意的是:我们要想改变state值,唯一的途径就是通过dispatch来派发action,再经过reducer返回改变后的state值,不能直接对state进行赋值操作!
补充:在没有引入中间件的情况下,我们dispatch派发的action只能是个对象,但如果有中间件增强,我们dispatch里就可以传一个函数进去来做些异步的操作!并且我们每派发一次dispatch,都会在内部调一次reducer
好了,接下来我们就对之前的代码做些调整:
index.js
import { legacy_createStore as createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
//注意,这里设置的初始值会被createStore处传入的初始值覆盖
const initState={title:'《我要借书》',num:1}
const rootReducer=(state=initState,action)=>{
const {type,payload}=action
switch (type) {
case "MING_DYNASTY":
return {...state,
num:payload.num,
title:payload.title
};
default:
return state;
}
}
const store = createStore(
rootReducer,
composeWithDevTools()
)
// 导出 Store 实例,
export default store
TestDemo.jsx :
import React from 'react'
import { Button } from 'antd';
import store from '@/store'
const actionType={
type: "MING_DYNASTY",
payload:{
title:"《明朝那些事儿》",
num:1
}
}
export default function GrandFDemo() {
return (
<div>
借的书名:{store.getState().title}
<br />
借了几本:{store.getState().num}
<br />
<Button onClick={() => { store.dispatch(actionType) }} >我要借书</Button>
</div>
)
}
从这张图里我们清楚的看到,redux里数据确实已经改变了。但是,我们视图层面上的值却并没有发生变化。这现象很好解释:react里视图层变化需要进行rerender,而rerender触发的条件不是state变了,就是props变了。此处我们想实现store中数据变更引起UI视图层数据变化得额外做些处理。好了,问题已经引申,下面就让我们开始解决这个问题吧!
3.3.3 触发 render
上一part我们提出了一个问题:明明redux里的数据已经发生了改变,但视图层没有进行相应的变化。那这一部分我们就来重点探索下项目里该怎么去解决这个bug。
首先我们也已清楚,视图层没发生变化是因为没有触发react的rerender。那既然这样问题就很好解决了,我们把store里的state值赋值给业务里的state就可以了,当store里数据值变化时再通过业务里的setState去改变业务的state值就能解决react没有rerender的问题。
但这样做真的能解决吗?
到这里各位小伙伴得先思考一个问题:我该怎么去判断何时进行触发rerender呢?最佳时机当然是监听到store里state发生改变后就进行rerender。那这又引入一个问题:我怎么知道store里的state数据发生了改变呢?如果我连监听时机都无法确定,还怎么去进行setState呢?
看到这里,估计有的小伙伴心里想“咱也不管你store里的数据什么时候变了,我就在你派发action之后就触发rerender。一句话,莽就完事了!”于是,就有了下面的代码:
import React , { useState } from 'react'
import { Button } from 'antd';
import store from '@/store'
const actionType={
同上...
}
export default function GrandFDemo() {
const [update,setUpdate]=useState({})
return (
<div>
借的书名:{store.getState().title}
<br />
借了几本:{store.getState().num}
<br />
<Button onClick={() => {
setUpdate({})
store.dispatch(actionType)
}} >我要借书</Button>
</div>
)
}
看到代码这一刻,咱脑海里飘过一段歌词:“你也没有错,只是不爱我~” 。这段代码没问题,确实解决了bug,强制rerender来进行视图层的数据更新。这必须是“代码和人,有一个能跑就行”,况且这还跑的挺好。
这种解法能覆盖大多数情况,可一旦后续我们想引入中间件来对action做改造,或者有些情况派发了action但我们state和之前仍一样,那还怎么写总归是不行的。原因在于:①引入中间件的目的大概率是要进行异步操作,而一旦有异步操作我们先强制render了,而后异步的更新才好,这样还是会造成视图不更新的情况。 ②如果我们派发的action里并没改变state,那用这种强制render的写法就相当于不管state更没更新都触发了界面的重新渲染。
所以,要真正解决这个bug,还是得去监测store里state的更新时机。等state更新之后,我们再进行rerender的操作去改变视图。至于我们刚才提出怎么知道store里的state数据发生了改变,这里store是给我们提供了api的,这就是大名鼎鼎的subscribe。
3.4 subscribe 监听 reducer
由于redux采用的是发布-订阅模式,我们可以使用subscribe去监听数据的变化。 它就相当于一个监听器,每次我们的reducer处理完action之后,subscribe都会去执行其内部的回调函数。 当然,subscribe也会返回一个取消订阅的函数,在我们组件卸载时需要进行取消订阅。
需要强调的是:subscribe并不在意state是否更新了,只要reducer处理完并return之后,subscribe里的回调都会执行。不会存在因为state没变就不执行的情况。并且,subscribe是在reducer处理完action之后执行的,也不是在派发action时!
使用方法如下:
const unSubscribe = store.subscribe(() => {
console.log('store里的state有变化');
})
知道了使用方法之后咱就上面的代码进行改造 :
import React, { useEffect, useState } from 'react'
import store from '@/store/index'
import { Button } from 'antd';
const actionType=() => {
...略
}
// 真实开发不会这么写,这里只是demo示例,下一部分切合业务会演示实际用法
export default function GrandFDemo() {
const [update,setUpdate]=useState({})
useEffect(() => {
const unSubscribe = store.subscribe(() => {
console.log('业务派发了action');
setUpdate({})
})
return ()=>{
unSubscribe()
}
},[])
return (
<div>
借的书名:{store.getState().title}
<br />
借了几本:{store.getState().num}
<br />
<Button onClick={() => {
store.dispatch(actionType())
}} >我要借书</Button>
</div>
)
}
注意: 我们在使用subscribe时,store.subscribe() 订阅的时机一定要在dispatch之前,否则就订阅不到数据变化了。如下面代码,我们在subscribe之前先dispatch了一下,会发现这个dispatch虽然修改了state数据,但是subscribe没有订阅到!
...
useEffect(() => {
store.dispatch(actionType())
// subscribe并没有订阅到dispatch的派发
const unSubscribe = store.subscribe(() => {
console.log('store里state有变化');
setUpdate({})
})
return ...
},[])
...
至此,我们对redux的基本用法也大致明白了。到这我们再来回顾总结下使用流程:
- 通过legacy_createStore创建store来存储我们的state;
- 业务里声明action去设置我们想要怎么操作store的state(通过dispatch派发);
- reducer接收state和action并根据action类型对应处理state并返回更新后的值;
- 业务中subscribe订阅了state的变化,并执行了回调;
- 业务里触发rerender去进行视图层面的更新;
上面一部分我们实现了在业务中使用redux,但是,我们按照上面的写法会有两个问题:
1、业务组件使用第三方创建的store对象应该要解耦的,最好是从外部传进来而不是直接导入,否则一旦有变动得去业务里一个个换;
2、代码层面冗余,需要在每个组件里都进行subscribe订阅。为了解决这些问题,redux官方其实给我们推荐了一个第三方库:react-redux。为了说明引入这个库的必要性,咱先来手写些代码去解决下上面两个问题。
3.5 优化写法
3.5.1 解耦store
我们业务使用时,redux的store对象最好是从外部传进去,那这自然而然就想到了Provider、Consumer的方式。将store作为Provider的value属性,哪个业务组件用到了直接Consumer消费即可。我们只改业务部分代码,store部分还是沿用上文中的,下面看代码:
context.js:
import React from 'react';
export const storeContext=React.createContext()
项目入口文件:
import React from 'react';
import ReactDOM from 'react-dom/client';
import store from '@/store'
import {storeContext} from '@/utils/context'
import App from './App';
import 'antd/dist/reset.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<storeContext.Provider value={store}>
<App />
</storeContext.Provider>
);
业务文件:
将原来直接导入store改为利用context来进行消费store。
App.jsx:
import TestDemo from '@/components/testComp/TestDemo'
function App() {
return (
<div className="App">
<TestDemo />
</div>
);
}
export default App;
TestDemo.jsx:
import React, { useEffect, useState, useContext } from 'react'
import {storeContext} from '@/utils/context'
import { Button } from 'antd';
const actionType=() => {
...略
}
export default function GrandFDemo() {
const [update,setUpdate]=useState({})
const storeConsumer=useContext(storeContext)
useEffect(() => {
storeConsumer.subscribe(() => {
console.log('store有变化');
setUpdate({})
})
},[])
return (
<div>
借的书名:{storeConsumer.getState().title}
<br />
借了几本:{storeConsumer.getState().num}
<br />
<Button onClick={() => {
storeConsumer.dispatch(actionType())
}} >我要借书</Button>
</div>
)
}
如上图所示,我们替换之后,代码没有问题。
3.5.2 抽离公共的订阅逻辑
上面我们也提到过,我们需要在每一个业务组件里都调用subscribe进行订阅才能监测到store的变化。为了抽取订阅逻辑,我们直接封装一个自定义hook来解决这个问题:
import {useState,useEffect,useContext} from "react"
import {storeContext} from '@/utils/context'
export const useStore=() => {
const storeConsumer=useContext(storeContext)
const [update,setUpdate]=useState({})
useEffect(() => {
storeConsumer.subscribe((val) => {
console.log('store有变化',val);
setUpdate({})
})
},[])
return {
storeConsumer
}
}
自定义hook完成后,我们在业务组件引入即可:
import React, { useContext } from 'react'
import {useStore} from '../useStore'
import { Button } from 'antd';
const actionType=() => {
...略
}
export default function GrandFDemo() {
const {storeConsumer} = useStore()
return (
<div>
借的书名:{storeConsumer.getState().title}
<br />
借了几本:{storeConsumer.getState().num}
<br />
<Button onClick={() => {
storeConsumer.dispatch(actionType())
}} >我要借书</Button>
</div>
)
}
3.6 实现connect函数
现在我们订阅的公共逻辑是抽取到自定义hook里了。除此之外,我还想拓展一个需求:能不能把store相关的一些api和我们props做一层映射,也就是说咱直接在业务组件里配一些prop就能去操作store里的数据(为了引出connect真是要老命了)。
既然这么提了,那必须是可以实现的!为了实现和connect函数用法类似的方法,这里咱就不用自定义hook去实现了。话不多说,且看分析:
总结一下我们刚才要拓展的那个需求,目的就是为了增强业务组件的props,使业务组件可以以一种黑盒的方式直接去操作store中的数据。一看到增强组件的props,一些小伙伴的dna可能动了:这不纯纯用高阶函数嘛。既然这样,那就关门,放高阶函数!
import React ,{PureComponent}from 'react'
import {storeContext} from '@/utils/context'
类写法:
export const connect =(mapStateToProps, mapDispatchToProps) => {
return (EnhancedComponent) => {
class WrappedComp extends PureComponent {
constructor(props,context) {
super(props);
this.state = {}
}
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({...this.context.getState()})
})
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return <EnhancedComponent {...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispatchToProps(this.context.dispatch)}/>
}
}
WrappedComp.contextType = storeContext;
return WrappedComp
}
}
或者用函数式写法:
import React ,{useEffect,useContext,useState} from 'react'
import {storeContext} from '@/utils/context'
export const connect=(mapStateToProps, mapDispatchToProps) => {
return (EnhancedComponent) => {
return (props) => {
const context=useContext(storeContext)
const [state,setState]=useState({})
useEffect(() => {
const unsubscribe = context.subscribe(() => {
setState({...context.getState()})
})
return () => {
unsubscribe()
}
},[])
return (
<div>
<EnhancedComponent {...props}
{...mapStateToProps(context.getState())}
{...mapDispatchToProps(context.dispatch)}
/>
</div>
)
}
}
}
看完代码,可能有的小伙伴这时候DNA就又动了:mapStateToProps、mapDispatchToProps 这个我熟呀,咱项目里用的就是这个来做映射的。该说不说,到此,咱就不知不觉实现了connect函数。手写完这个高阶函数之后,再去用第三方库的现成API感觉就不会突兀了!下面我们结合手写版的connect函数对业务组件进行调整:
import React, { useContext,useState } from 'react'
import { Button } from 'antd';
import {connect} from '../connect'
const actionType=() => {
return {
...略
}
}
function GrandFDemo(props) {
return (
<div>
借的书名:{props.title}
<br />
借了几本:{props.num}
<br />
<Button onClick={() => {
props.borrowBook(actionType())
}} >我要借书</Button>
</div>
)
}
function mapStateToProps(sharedData) {
return{
title: sharedData.title,
num:sharedData.num
}
}
function mapDispatchToProps(dispatch){
return{
borrowBook:(action) => {
dispatch(action)
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(GrandFDemo)
从上面代码我们就可以发现,利用connect函数之后,我们就可以直接把一些和共享数据相关的拎到mapStateToProps、mapDispatchToProps而且我们再想操作共享数据时,就可以直接从自身props解构出来进行操作了。简单好用,一个字总结就是~~~6 。

四、引入react-redux库
上面一part我们虽然实现了store的解耦、connect函数的手写。但我们实际项目中,为了追求效率和代码逻辑的健壮性都会去引入react-redux这个第三方库来辅助我们开发,上面介绍了那么多也是为了帮助我们理解的更深刻。
老规矩,先下载react-redux:
yarn add react-redux
多提一句:在我们实际项目中,并不是一定要用react-redux,虽然其给我们提供了很多便利,但是这也意味着咱还得去看下 react-redux里面相关的用法。
好了,接下来咱就开始介绍下react-redux里的相关api,也看看它到底是有多便捷(我们的store相关数据还是沿用之前的)。
4.1 Provider:共享store
react-redux里有一个Provider组件,其作用是将我们的redux里的store共享给所有的业务组件,我们的store只有经过Provider共享了才能被react-redux其他钩子获取使用。
Provider用法很简单,将我们在代码实现那一par自己维护的storeContext.Provider 替换成react-redux里的Provider即可。注意,react-redux中的Provider是使用store这个key值来传递共享中心store的;我们之前是用value来共享store的。
import { Provider } from 'react-redux';
import App from './App';
import store from '@/store'
...
...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
4.2 useStore: 获取store
上面我们把store共享给了全局,但是怎么在业务去拿这个store呢?这不useStore虽迟但到!
useStore是react-redux给我们这些玩家提供的一个获取当前项目store的钩子,我们利用这个钩子就可以去调用store上的dispatch、getState 等Api:
import React from 'react'
import {useStore} from 'react-redux'
import { Button } from 'antd';
const actionType=() => {
return {
...同上
}
}
export default function Demo2(props) {
const store=useStore()
console.log('>>>>',store);
return (
<div>
借的书名:{store.getState().title}
<br />
借了几本:{store.getState().num}
<br />
<Button onClick={() => { store.dispatch(actionType()) }}>我要借书</Button>
</div>
)
}
4.3 useSelector:获取state,视图更新
之前我们说过,redux用的是订阅发布模式,如果不用subscribe订阅store的数据更新,从而在回调进行render的话,我们派发action后,只会改数据而不会引视图层的更新。那既然引入了react-redux,我们还需要手动订阅吗?还是那句话:那肯定不需要呀!
react-redux给我们提供了一个获取state的钩子函数useSelector。此钩子接收一个回调函数作为参数,回调的第一个参数就是我们的store所有的state值,返回值就是我们要取所有state值里的哪一个state值。
用法如下所示:
import {useSelector} from 'react-redux'
...
const globalnum=useSelector((allGlobalData)=>allGlobalData.num)
...
useSelector的获取state值的特性不是我想要重点说的。useSelector部分的主菜是它不仅能获取store里的state值,还能进行视图层面的render更新!!在useStore部分我们如果调试了就会发现,单凭store.dispatch、store.getState()不能引起rerender,想要改变视图还要我们手动去触发。
咱先上一段代码大家体会体会:
import React from 'react'
import {useStore,useSelector} from 'react-redux'
import { Button } from 'antd';
const actionType=() => {
return {
type: "MING_DYNASTY",
payload:{
title:"《明朝那些事儿》",
num:1
}
} }
export default function Demo2(props) {
const store=useStore()
const data=useSelector((store) => {
return store.title
})
console.log('视图render了>>>>');
return (
<div>
借的书名:{store.getState().title}
<br />
<br />
<Button onClick={() => { store.dispatch(actionType()) }}>我要借书</Button>
</div>
)
}
从上面这张图我们可以看到视图层发生了变化,但按道理来说视图是不会变化的:
1、首先我们是使用useStore来进行取值或者派发,useStore不会引起视图层render的;
2、其次我们只声明了useSelector返回出来的data值但还没有使用它,但当我们派发时视图就render了。后续当我们当我们再次单击我要借书按钮时,视图层又不进行render了,这现象属实奇怪。
有的小伙伴心里可能有个猜测:useSelector内部是不是有强制render的方法,当满足某些条件时就会进行触发从而刷新视图。咱就是说“盲生,你发现了华点” 。useSelector内部的处理这里咱们不做深入探讨,毕竟都能另开一篇文章了,简单总结就是:useSelector注册了个订阅,当我们state被操作时,此订阅就会被调用,若是这次操作导致useSelector返回值(注意,是useSelector的返回值对应这里就是data值)发生了改变,此时就会触发一次rerender改变视图并返回一个新值。
useSelector源码里使用 checkForUpdates 来确认本次操作是否更新了返回值 ,若更新了则进行 forceRender。感兴趣的小伙伴们可以去看下react-redux的源码。
到这里可能会有人提问:如果我每次都返回对象呢?这样每次都是一个新的引用呐,还会触发forceRender吗?害,不愧是盲生,又发现了华点。确实,真要是返回对象那每次都会forceRender,为了避免这种情况,我们可以在useSelector的第二个参数上传入shallowEqual来解决对象每次render的问题:
const data=useSelector((globalState) => {
return {title:globalState.title }
},shallowEqual)
补充:何时用useStore、何时用useSelector ?
useStore使用场景:
我们在实际项目中,有些地方的共享数据可能只作为参数进行组件内部赋值此时我们就没必要用useSelector进行取值,而是用useStore的getState方法去获取值。这样做的好处是避免了引入useSelector触发组件频繁rerender的问题。
useSelector使用场景:
当然,如果我们的共享数据要作为UI层的展示数据,并且也涉及到更新之类的情况,此时我们则用useSelector来去获取store里的state值。
4.4 useDispatch:派发action
useDispatch钩子的作用非常直白:返回dispatch进行派发action。这个钩子可以让我们在不引入store的情况下直接用store对象里的dispatch方法。用法如下:
import { useDispatch} from 'react-redux'
...
const dispatch=useDispatch()
...
4.5 connect:连接store操作与业务props属性
我们刚刚也手写了connect函数,其实react-redux已经给我们提供了的,使用方法和我们手写版的一模一样。将想要获取的state值维护在mapStateToProps中,更新store里state数据的方法维护到mapDispatchToProps中。同样的,我们使用了connect函数后,就不用手动去订阅store了,因为connect内部帮我们做了。下面看用法:
import React from 'react'
import {connect} from 'react-redux'
import { Button } from 'antd';
const actionType=() => {
return {
type: "MING_DYNASTY",
payload:{
title:"《明朝那些事儿》",
num:val
}
} }
function Demo(props) {
return (
<div>
借的书名:{props.title}
<br />
借了几本:{props.num}
<br />
<Button onClick={() => { props.borrowBook(actionType()) }}>我要借书</Button>
</div>
)
}
function mapStateToProps(sharedData){
return {
title:sharedData.title
num:sharedData.num
}
}
function mapDispatchToProps(dispatch){
return{
borrowBook:(action) => {
dispatch(action)
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Demo)
我们可以看出,业务组件中使用了connect函数后,组件就变成了一个ui组件,一些业务逻辑就可以完成交给mapStateToProps、mapDispatchToProps来处理了!实际开发中,我个人偏向用useSelector和useDispatch来进行redux数据里的相关操作的。当然,如果想要把业务逻辑与组件层解耦,也可使用connect来进行相关操作。
五、redux功能增强
5.1 combineReducer:拆分reducer
我们首先要思考一个问题:随着我们项目的迭代,store里的reducer会处理越来越多的action,那时我们一个reducer可能会出现两三千行的情况,而且还全都掺着一堆判断。嘶~恐怖如斯,那我们应该怎么解决这个问题呢?肯定多招些图书管理员呗!
我们使用combineReducer 的目的就是为了让我们能有多个管理员也即可以帮助我们去拆分项目里的reducer。有了combineReducer之后,我们就能根据业务里的各个模块,将reducer对应模块进行拆分。每个模块的reducer维护每个业务模块对应的action和state,这样既利于维护,代码可读性也更高。
首先要清楚combineReducer就是个函数,此函数接收各个模块的reducer作为参数。一般的我们建议将各个模块的reducer维护到一个对象中并作为参数传入combineReducer中。然后把combineReducer的返回值作为此项目的总reducer 传入store中。好了,下面咱就用combineReducer来改造下上文的store文件:
store/index.js:
import { legacy_createStore as createStore,combineReducers } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
// 1、这个bookReducer是我们业务里 book模块相关的reducer
const initBookState={title:'《我要借书》',num:1}
const bookReducer=(state=initBookState,action)=>{
const {type,payload}=action
switch (type) {
case "MING_DYNASTY":
return {...state,
num:payload.num,
title:payload.title
};
default:
return state;
}
}
// 2、userReducer是业务里 user模块相关的reducer
const initUserState={userID:'001',userName:'张添财'}
const userReducer=(state=initUserState,action)=>{
const {type,payload}=action
switch (type) {
case "ROOT_USER":
return {
...state,
userID:payload.userID,
userName:payload.userName
};
default:
return state;
}
}
// 用combineReducers整合各模块的reducer
const rootReducer=combineReducers({
book:bookReducer,
user:userReducer
})
const store = createStore(
rootReducer,
composeWithDevTools()
)
// 导出 Store 实例,
export default store
注意:我们用combineReducers改造reducer的同时,也要注意区分各自模块的action的type类型,不能出现重复现象!!并且由于是将项目reducer拆分成如上combineReducers({ book:bookReducer, user:userReducer })
的形式,所以我们在业务中取值也要相应取对应key:store.getState().book / store.getState().user
但是我们在各自的bookReducer、userReducer 中不用再区分book还是user,因为已经是各自维护本模块的state了。也既reducer不用写成如下形式:
...
const bookReducer=(state=initBookState,action)=>{
...
switch (type) {
case "MING_DYNASTY":
return {
...state,
// 这里不需要写成这种形式
book:{
num:payload.num,
title:payload.title
}
};
...
}
}
const userReducer=(state=initUserState,action)=>{
...
switch (type) {
case "ROOT_USER":
return {
...state,
// 这里不需要写成这种形式
user:{
userID:payload.userID,
userName:payload.userName
}
};
...
}
}
...
5.2 增加redux-thunk中间件支持异步请求
在我们真实开发中有些数据是拿服务端响应回来的数据的。这就要求redux要能支持异步。但默认情况下,redux是不支持异步请求数据的,也就是说从dispatch派发action --> reducer处理这个流程都是同步进行的。为了解决这个问题,redux允许我们使用中间件来在dispatch派发的action最终达到reducer之前进行功能拓展。
好了,此处我们引入 redux-thunk 来让redux支持一些异步操作。
下载安装redux-thunk:
yarn add redux-thunk
store引入redux-thunk中间件:
import { legacy_createStore as createStore ,applyMiddleware} from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import rootReducer from './reducer'
// 创建 Store 实例
const store = createStore(
// 参数一:根 reducer
rootReducer,
// 参数三:增强器,加中间件
composeWithDevTools(applyMiddleware(thunk))
)
// 导出 Store 实例
export default store
这里我们要先了解下redux-thunk的整体使用流程:默认情况下,我们redux里的action都必须是一个对象。但是react-thunk可以允许我们的action既可以是一个对象,也可以是函数。当我们的action是一个函数时,它会接收两个参数dispatch和getState。我们把函数形式的action传到业务组件里的dispatch时,其内部会主动对action进行调用,不用我们去手动调。
前提:引入react-thunk
const mockRequest=()=>{
return new Promise((resolve)=>{
setTimeout(()=>{
resolve({title:"《三体》",num:1})
},2000)
})
}
const actionSync=(payload)=>({
type:"SANTI",
payload
})
// 这里入参的dispatch就相当于store里的dispatch;getState就是store里的getState函数
const actionAsync=async(dispatch,getState)=>{
console.log('这是当前的state的值',getState())
const res=await mockRequest()
dispatch(actionSync(res))
}
业务文件:
...
const dispatch=useDispatch()
...
...
// 这里不用我们主动去调用actionAsync!!
dispatch(actionAsync)
...
以上就是我们使用中间件来增强redux的全部步骤。不过各位小伙伴还是结合自己的业务去引入中间件,毕竟就算不引入redux-thunk咱还是可以在业务里调接口后再去派发action嘛!
六、业务实操-模块化拆分
在上面部分我们都是把reducer、store合并在一个文件中。但我们现在都是模块化开发,所以我们在使用redux的时候也要进行相应的模块化拆分。一般地,我们会将整个store文件拆分成index.js、reducer、action三个模块文件,这里我们就对之前的store文件进行改造拆分。
首先,我们把reducer拎出来单独创建文件夹,再根据业务模块来创建不同的reducer文件。
拆分book模块的reducer:
|---reducer
|---bookReducer.js
const initState={title:'《我要借书》',num:1,allBooks:[]}
const bookReducer = (state=initState, action) => {
const {type,payload}=action
switch (type) {
case "MING_DYNASTY":
return {
...state,
num:payload.num,
title:payload.title
};
case "ALL_BOOKS":
return {...state, allBooks:payload.allBooks};
default:
return state;
}
}
export default bookReducer
拆分user模块的reducer:
|---reducer
|---userReducer.js
const initState={userID:'001',userName:'张添财'}
const userReducer = (state=initState, action) => {
const {type,payload}=action
switch (type) {
case "ROOT_USER":
return {
...state,
userID:payload.userID,
userName:payload.userName
};
default:
return state;
}
}
export default userReducer
拆分reducer文件夹的入口文件,合并reducer:
|---reducer
|---index.js
import { combineReducers } from 'redux'
import bookReducer from './bookReducer'
import userReducer from './userReducer'
const rootReducer=combineReducers({
book:bookReducer,
user:userReducer
})
export default rootReducer
同时在store中引入rootReducer:
import { legacy_createStore as createStore ,applyMiddleware} from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
import rootReducer from './reducer'
// 创建 Store 实例
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
)
// 导出 Store 实例
export default store
其次,创建action文件夹,根据个业务模块创建actions:
book模块的action:
|---actions
|---book
|---booksActionCreator.js
const mockRequest=()=>{
// 模拟接口请求
return new Promise((resolve)=>{
setTimeout(()=>{
resolve([{title:"《明朝那些事儿》",num:10},
{title:"《三体》",num:8},
{title:"《时间简史》",num:20}])
},1000)
})
}
export const borrowBooksAction=(val=1)=>{
return {
type: "MING_DYNASTY",
payload:{
title:"《明朝那些事儿》",
num:val
}
}
}
const allBooksAction=(data)=>{
return {
type: "ALL_BOOKS",
payload:{
allBooks:data
}
}
export const fetchAllBooksAction=async()=>{
return (dispatch)=>{
const res=await mockRequest()
dispatch(allBooksAction(res))
}
}
user模块的action:
|---actions
|---user
|---userActionCreator.js
export const borrowBooksAction=()=>{
return {
type: "ROOT_USER",
payload:{
userID:"007",
userName:'添财青年'
}
}
}
最后,在业务组件进行派发action即可:
Demo.jsx:
import React from 'react'
import {useSelector, useDispatch} from 'react-redux'
import {borrowBooksAction} from '@/redux/reducer/booksActionCreator'
import { Button } from 'antd';
import Demo from './Demo'
function Demo(props) {
const data=useSelector((store) => {
return {title:store.book.title,num:store.book.num,allBooks:store.book.allBooks }
},shallowEqual)
const dispatch=useDispatch()
return (
<div>
借的书名:{data.title}
<br />
借了几本:{data.num}
<br />
<Button onClick={() => { dispatch(borrowBooksAction(1)) }}>借明朝</Button>
<Button onClick={() => { store.dispatch(fetchAllBooksAction()) }}>获取全部可借图书</Button>
<br />
这是全部的可借图书:{
data.allBooks.map((item)=>{
return (
<div>
item.title
<br />
</div>
)
})
}
</div>
)
}
export default Demo
好了,至此,我们redux部分就到此结束了。在我们的实际开发就是按照业务实操这一part进行文件拆分、store数据分发的,各位多加练习,问题都不大的!
整个store模块的目录结构:
|---store
|---index.js
|---reducer
|---userReducer.js
|---bookReducer.js
|---actions
|---book
|---booksActionCreator.js
|---user
|---userActionCreator.js
结语
能看到这里的小伙伴属实难得,在写到这里的时候本来还想顺着思路介绍下RTK,奈何写的时间太长了,精力被榨干了,没办法,咱只能后续有机会再聊聊RTK了。年底了,回想22年初给自己定的各种计划真是让人汗颜!过完了兵荒马乱的2022,咱期待着明年一切都会好起来的吧。又是那句话:修炼内功,砥砺前行,各位继续努力呀!
转载自:https://juejin.cn/post/7178678099901415483