第7章:redux

7.1. redux理解

在没有学习redux之前,我的一些感想:

redux其实是一个单独的js库,用在react里面的就是react-redux库,都是同一个团队的作品。

redux其实和vuex或pinia一样,都是用作状态管理的工具。官方介绍:“React ReduxRedux 的官方 React UI 绑定库。它使得你的 React 组件能够从 Redux store 中读取到数据,并且你可以通过dispatch actions去更新 store 中的 state。”。所以用起来应该不难,但是用好就不容易了,想要用好,就先看一看别人项目的例子吧。

7.1.1. 学习文档

  1. 英文文档: https://redux.js.org/
  2. 中文文档: https://cn.redux.js.org/https://www.redux.org.cn/
  3. Github: https://github.com/reactjs/redux

7.1.2. redux是什么

  1. redux是一个专门用于做状态管理的JS库(不是react插件库)。

这里的状态管理是什么?你想到了什么?

提到状态,就要想到组件里面的state,这就是组件的状态。那么redux就是将组件的共享状态集中进行管理(组件把一些状态交给redux进行管理,不意味着自己不能有别的状态,自己的一些非共享状态还是该有就有),需要这些状态时,就从redux来拿。

为什么需要状态的集中管理呢?

有一些组件之间的通信,各自嵌套的很深,联系起来很麻烦。是可以使用PubSubJS,但是多个组件之间消息发布与订阅,还是麻烦。于是就出现了redux。比如说用户的登录信息,我在login页面登录的时候就拿到了,但是我会在多个别的页面进行使用,这些页面难道都与login页面进行消息发布与订阅吗?肯定有更好的办法,我把用户的登录信息放在状态集中管理的地方,别的页面要用就去拿,这不是更好吗?

  1. 它可以用在react, angular, vue等项目中, 但基本与react配合使用。
  2. 作用: 集中式管理react应用中多个组件共享的状态。

7.1.3. 什么情况下需要使用redux

  1. 某个组件的状态,需要让多个其他组件可以随时拿到(共享)。
  2. 一个组件需要改变另一个组件的状态(通信)。
  3. 总体原则:能不用就不用, 如果不用比较吃力才考虑使用。

7.1.4. redux工作流程(重点)

如果这个流程搞不清楚,就算我再死记硬背,还是非常容易忘记。一定要多看几遍视频。

image-20211118163827231

看了这个流程,我想到了vuex或者pinia,我只谈它们的写法,可以创建多个仓库,每个仓库里面的初始化数据、方法等信息都在同一个文件里面,改起来非常方便。

但是这个redux,要把很多文件连接起来,我看了官方的redux的写法,有actions文件夹,constants文件夹,reducers文件夹,containers文件夹,分别放不同的文件,要联系起来真是不容易。为什么要这样设计呢?其实react的设计的思想就是追求小粒度控制,vuex和pinia为什么好用呢?因为他们的设计思想就是封装大量api,直接调用api即可,但是这样涉及到大量计算,所以性能方面会有影响。redux其实把“状态管理”的真实实现过程大部分交给程序员了,里面的优化就靠程序员的水平了,确实对JS要求很高啊。

所以更要将这个流程理解清楚。这个过程理解清楚了,其实vuex和pinia的过程也容易理解了。

7.2. redux的三个核心概念

7.2.1. action

  1. 含义:动作的对象
  2. 包含2个属性

type:标识属性, 值为字符串, 唯一, 必要属性

data:数据属性, 值类型任意, 可选属性

  1. 例子:{ type: 'ADD_STUDENT',data:{name: 'tom',age:18} }

7.2.2. reducer

  1. 含义:用于初始化状态、加工状态。
  2. 加工时,根据旧的state和action, 产生新的state的纯函数

7.2.3. store

  1. 含义:将state、action、reducer联系在一起的对象

  2. 如何得到此对象?

    1)import {createStore} from 'redux'

    2)import reducer from './reducers'

    3)const store = createStore(reducer)

  3. 此对象的功能?

    1)getState(): 得到state

    2)dispatch(action): 分发action, 触发reducer调用, 产生新的state

    3)subscribe(listener): 注册监听, 当产生了新的state时, 自动调用。使用this.setState({})让组件重新渲染

7.3. redux的核心API

7.3.1. createstore()

作用:创建包含指定reducer的store对象

7.3.2. store对象

  1. 作用: redux库最核心的管理对象

  2. 它内部维护着:

    1)state

    2)reducer

  3. 核心方法:

    1)getState()

    2)dispatch(action)

    3)subscribe(listener)

  4. 具体编码:

    1)store.getState()

    2)store.dispatch({type:'INCREMENT', number})

    3)store.subscribe(render)

7.3.3. applyMiddleware()

作用:应用上基于redux的中间件(插件库)

7.3.4. combineReducers()

作用:合并多个reducer函数

7.4. 使用redux编写应用

效果

7.4.1 求和案例-纯react版

纯react版,就是在一个组件中写状态和更新状态的方法。根据之前的规则:状态在哪里,更新状态的方法就在哪里。

写这个,可能刚开始获取select里面的选项值还不熟练,需要搜索答案。其余的应该没有问题。

 

7.4.2 求和案例-redux精简版

精简版省略掉了Action creators,为什么呢?因为action其实就是JS里面的Object对象,在此案例中完全可以模拟出来,着重了解其它的原理。

1、安装redux:npm install redux

2、编写组件内容:无论是交出共享状态的组件,还是要使用到共享状态的组件,首先不要考虑redux该怎么写,直接把外形框架写出来,里面该怎么写就怎么写,那么用到共享状态的地方,可以先搭好架子空出关键部分。这样就比较好写了,否则一开始就考虑到redux,可能写很多次之后才能顺畅。

3、编写store

在redux中,store起到枢纽的作用,在这里只将reducer引进来,actions先不考虑。

新建一个redux文件夹,和App.jsx是同层级的。

这里有一句话:“整个应用只有一个store对象”,如果我有多个组件,分别有不同的状态进行共享,该怎么做呢?

后面会讲到combineReducers(),可以解决这个问题。

还有,store.getState()得到的是哪一个组件共享的数据呢?后面看一下解决方案。

仔细看一下redux的原理图,里面的actions是通过dispatch方法提交给store的,只有reducers是双向的,是不是说明createStore()里面只需要引入reducers呢?

那么我看了examples里面,老师讲解里面,是这样的,createStore()里面不需要引入actions。

4、编写reducer

问题:这里为什么没有定义“奇数加”和“异步加”的方法呢?

7.8中会讲,redux中的reducer函数必须是一个纯函数。

5、通过redux的api来 do what?和getState()(这是原理图里面的话)

redux的状态更改默认不会触发页面更新render,因为redux不是Facebook出的(虽然redux的作者是react的维护者之一),所以没有对它进行特别处理,怎么办?

使用store.subscribe()方法来订阅redux中状态的更新,触发this.setState({})事件,不会更新任何本组件的state数据,但是会触发页面重新渲染。

优化:

如果有很多个组件需要获取到最新的redux状态,怎么办呢?在每一个组件中的componentDidMount(){}中都写上store.subscribe()吗?有没有简单的方法呢?

有,在项目的index.js中统一写。

这样做了之后,整个App都要更新,虽然有diffing算法,但是给人感觉就是引起了没有必要的更新,这里还是要看一下有没有更好的方法,或者说是不是这样其实也没有影响。

 

后记:

老师把整个写redux的流程介绍的非常清楚,先写最重要的store,然后写reducer,最后在组件里面使用store上面的api,将功能联系起来。

对着原理图,多看几遍视频,多做几次。

7.4.3 求和案例-redux完整版

完整案例是加上了actions。reducers和store都不用更改。

怎么做呢?精简版的action是模拟生成的,怎么模拟的?就是模拟一个对象而已,里面有type和data。type有几种类型?就两种类型,data是获取到的select的值,那么我就可以定义函数,data做参数,返回一个Object不就行了吗?有两种type,就定义两个函数。(当然可以通过switch来判断写成一个函数,但这是为了后面的异步action的写法,写成了两个)

那么使用的时候,就调用函数,传递select值做参数,生成一个Object就行了。就是这么做。

另外,由于多处用到了字符串"increment"和"decrement",所以为了避免拼写错误,新建一个文件,专门放常量,在使用的时候,直接使用这些常量。这也算是一种编程规范吧。

那么在组件里面生成action的方法也要跟着改变:

7.5. redux异步编程

原理图中说明了action是一个对象,里面有type和data属性。但是action还有一种类型,那就是function类型(需要结合中间件一起才能使用)。

一般redux同步编程时,使用对象类型的action;redux异步编程时,使用function类型的action。

7.5.1理解:

  1. redux默认是不能进行异步处理的,需要使用异步中间件。
  2. 某些时候应用中需要在redux中执行异步任务(ajax, 定时器)

7.5.2. 使用异步中间件

作用: redux的store本身是不能处理异步action的,需要借助异步中间件,帮助处理。

安装:npm install --save redux-thunk

在store.js中配置中间件。

7.5.3 在具体action文件中编写异步action

优化:

进一步优化:

7.5.4 在组件中使用异步action

小结:

 

7.6. react-redux

注意:react-redux的基础是redux,之前学习的内容是基础,react-redux只是简化了一些操作而已,不要以为react-redux是用来取代redux的,不是的。

image-20230815220247544

整个react-redux的内容,老师讲的是很慢的,真正要写的代码其实是一步一步优化得来的。这种慢是非常必须的,因为其中的关键节点为什么要这么做,老师讲清楚了,我才好理解。老师完全可以一步到位,怎么做直接告诉我,这里这么写,那里那么写,但是我很难理解,理解不了就很难再写出来,也容易忘记。

每一小节涉及的代码并不多,但概念还是很多的。所以耐心点,听一遍听不懂,就听第二遍,第三遍......直到真正掌握。我的目的不应该是快速学完,而是真正掌握。

react之所以要设计容器组件,就是为了让UI和redux分离开来,让UI组件干干净净的,应该是react团队的一种设计思想,理解并运用这种设计思想就行了。可以这么理解:react团队是有追求的人,不可能让外部的东西来随意干扰自己的代码,对react的核心要有绝对的掌握。(虽然redux团队和react团队渊源很深)

7.6.1. 理解

  1. 一个react插件库,Facebook官方出品。
  2. 专门用来简化react应用中使用redux

上面说了react-redux是用来简化react应用中使用redux的流程的,但是呢,老师讲的很慢,造成我的一个感觉:怎么比单纯使用redux更麻烦、更复杂呢?

因为老师想把react-redux实现的过程讲清楚,等学到最后的时候,我肯定会豁然开朗的。

不要嫌老师讲的慢,讲的慢我才能消化,今天看了一下react-router-dom和redux的官方examples,很多用法我还是没有见过的,又看了一下antd的官方仓库代码,更是看不懂了。我是一定要看官方examples和antd官方仓库代码的,因为它们代表了一定的高水平,我如果写的代码太烂,remote工作被裁员也是很有可能的,所以一定要达到一定的水平才行。

深研JS比我想象的难得多,所以一直没有做、不敢做、不知道该怎么做,但是现在有一个月薪10万的机会放在眼前,为什么不做呢?可能看清楚antd编码风格,还需要学习一下设计模式,所以要做的还有很多啊。

7.6.2. react-Redux将所有组件分成两大类

  1. UI组件

    1)只负责 UI 的呈现,不带有任何业务逻辑(这个“不带有任何业务逻辑”,指的是什么呢?这一点要理解清楚。这里应该指的是不带有业务实现的具体逻辑代码,具体实现逻辑放在redux中,由容器组件来连接UI组件和redux,UI组件中只能用 this.props.方法 来调用实现。如果你理解成为不写任何function,那怎么实现功能呢?怎么触发功能呢?还是需要依靠UI来触发事件的吧?)

    2)通过props接收数据(一般数据和函数)

    3)不使用任何 Redux 的 API

    4)一般保存在components文件夹下

  2. 容器组件

    1)负责管理数据和业务逻辑,不负责UI的呈现。(注意:是“管理数据和业务逻辑”,只是管理而已,真正的共享数据和逻辑放在redux中)

    2)使用 Redux 的 API

    3)一般保存在containers文件夹下

注意:下面的例子中,从7.6.2.1-7.6.2.5,都是在之前案例基础上编写的,里面编写的redux文件夹里面的代码大部分并没有进行修改,如果有修改,我会列出来,因为react-redux首先要依靠redux才行,然后才是使用方式的简化处理。

7.6.2.1 连接容器组件与UI组件

这一步首先实现“容器组件”与UI组件的连接。从上一小节可以看出,UI组件不应该有任何业务逻辑,所以UI组件定义的方法里面涉及到redux的代码都要清除掉。需要新建containers文件夹,里面新建 Count/index.jsx 文件,里面写容器组件的代码。容器组件怎么实现呢?先考虑最简单的情形:联系redux和UI组件,然后暴露出一个组件。就要用到react-redux里面的connect()()方法来帮助生成一个组件(容器组件不需要自己写)。

image-20230815222600126

1、安装:`npm i react-redux

2、编写UI组件,里面不应该包含redux的任何东西,包括actions、store等相关的所有东西。

注意:store.subscribe()方法如果放在了UI组件中的componentDidMount(){}里面,就需要转移到项目的index.js文件里面。

3、编写容器组件,并暴露出容器组件。

store并不是直接在容器组件里面直接引入的,而是通过父传子的props传递过来的。固定套路(为什么这么写呢?因为用到了connect()()函数,当我们在里面传入函数做参数时,会在函数参数上自动传入store相关的状态和方法,这是一种设计模式,后面你会看到)。

4、在App.jsx中引入容器组件,并传递store给容器组件。

原来App.jsx里面引入的是UI组件Count,现在要改为容器组件Count,因为容器组件已经把UI组件联系起来了。

经过上面的处理之后,容器组件和UI组件已经联系起来了。打开网页没有报错,OK。

7.6.2.2 react-redux的基本使用

map除了“地图”的意思,还有“映射”的意思。那么mapStateToPropsmapDispatchToProps就很好理解了。

映射state到props,映射dispatch到props。那么就可以在UI组件中使用 this.props.方法 来实现逻辑。

在7.6.2.1中我们已经实现了容器组件连接UI组件,连接redux只是将store传递给了容器组件,无论是容器组件与UI组件还是容器组件与redux,它们之间的具体功能还没有实现。这一步来实现容器组件连接UI组件和redux,并实现功能。

1、容器组件给UI组件传递redux中保存的状态和操作状态的方法。

难点:怎么做呢?容器组件和UI组件是父子组件关系,父组件给子组件传递数据和方法,在之前的学习中,我们知道要通过标签属性传递,可是容器组件里面没有标签啊,容器组件是connect()()函数生成的,怎么办?

答案:connect()()函数规定了,需要在第一次调用的时候,传递两个函数进去。什么叫“第一次调用的时候”?就是connect()执行的时候,它执行之后会返回一个函数,可以再次调用。

所以需要在connect()()函数的第一个()里面传入两个函数。函数是什么呢?怎么写呢?仔细观察父组件给子组件传值,是通过标签属性传值的,本质上是一种key:value的形式传递的,是不是?<Count store={store} />,等号左边是key,等号右边是value,然后子组件里面可以通过 this.props.key 的方式拿到value。那么就可以通过返回对象的方式,传递数据和方法。

以下先不考虑redux里面的实际数据和方法,第二步会取到store里面的数据和方法,这里只是说明应该传递对象过去,而且子组件可以收到。

第一个函数是传递redux中所保存的状态。

第二个函数是传递redux中操作状态的方法。

将两个函数作为参数,写到connect()()中。

完整代码:

在UI组件输出 this.props,看拿到状态和方法没有。

image-20230817104422893

这里的store是connect()()处理之后传递过来的,不是我的操作。后面的优化会用到这个属性。

2、容器组件联系redux

App.jsx里面已经传递store给容器组件了,那么容器组件里面不需要再引入了。但是怎么使用呢?如果是class类组件,要在类里面使用this.props,如果是函数式组件,要在函数参数里面写props才能使用。可是这里是connect()()生成的一个组件,我怎么用store?

答案是:在connect()()第一次调用传递的两个函数中,直接使用store里面的数据或方法。react-redux其实在这两个函数中传递了store里面的数据或方法。固定套路。第一个函数默认有state参数,第二个函数默认有dispatch参数。

没有必要在映射的时候定义action,actions都在actionCreator文件中定义好了,直接引入使用即可。

3、UI组件里面使用容器组件传递过来的数据和方法

使用方式: this.props.

检查效果OK。

7.6.2.3 优化1

对connect()()第一次调用时传入的两个函数进行优化,写成箭头函数的简写形式:

进一步简写,不用定义函数了,直接将函数写在connect()()的第一个()中:

前面说过了,connect()()第一次调用的时候,传入的两个参数都是函数,针对第二个参数说明一下。

按照函数的写法,当UI组件使用操作状态的方法时this.props.increment(data),实际上使用的是这样的方法:(data) => dispatch(createIncrementAction(data)),使用的是一个函数。

现在规定了,connect()()第一次调用的时候第二个参数可以为一个对象,对象里面属性的key还是不变,但value变为actionCreator(也就是返回一个action),写成这样:

那么传递到UI组件的到底是什么呢?输出看一下:

image-20230817135400645

image-20230817135710410

对比一下第二个参数是函数时的输出:

image-20230817135532712

image-20230817135624042

可以看到,如果第二个参数是对象,那么传递到UI组件的increment等属性的value都变成了函数,原因是react-redux识别到了第二个参数是对象,就帮助处理了action,加上了dispatch方法。

所以无论第二个参数是写成函数还是对象的形式,UI组件使用的方法都不变。

7.6.2.4 优化2

1、之前在react项目的index.js文件里面要写这段代码:

目的是订阅redux状态的更新,重新渲染页面(因为redux不是react官方出品的,react没有做相应的处理,redux的状态更新不会引起重新渲染)。

现在使用了react-redux,这段代码就可以清除了,因为react-redux针对redux状态的更新做了处理,不需要单独处理了。

2、需要传递store给容器组件,是在App.jsx里面容器组件的标签上通过标签属性的方式传递的,如果有多个容器组件,都要这样写,太麻烦了。现在react-redux提供了一个Provider标签,可以为App.jsx下所有的容器组件提供store这个属性,需要在项目的index.js里面这样写,App.jsx的子组件标签上的store属性可以去掉了。

7.6.2.5 优化3

一个文件里面可以写多个组件,于是可以将容器组件和UI组件写在一个js/jsx文件里面,最终暴露的是容器组件,所以可以将容器组件中引入的UI组件去掉,不引入,直接在容器组件的文件里面写UI组件。

 

7.6.2.6 数据共享-编写Person组件

redux可以管理多个共享状态,之前的案例只从最简单的情况入手,管理了count组件里面的共享状态count,那么别的组件的共享状态同样可以由redux进行管理,需要用到combineReducers()方法,对各个reducer进行合并。

那么根据之前的知识就可以得出:要创建相应的actionCreators.js和reducer.js文件,常量都可以放在一起constants.js文件中,store.js整个应用只有一个。

那么各种actionCreators.js和reducer.js就会有很多,老师建议在redux文件夹中创建action和reducer文件夹,文件名就写组件的名字,不需要带上action或reducer,因为文件夹名已经体现了具体的文件含义,类似这样:

需求:

1、Person组件里面有姓名、年龄输入框,“添加”按钮。同时展示Persons的列表。

2、在Count组件中添加展示Persons的信息,在Person组件添加展示Count的当前结果。

下面先创建Person的UI组件(容器组件和UI组件写在一个文件中):

为person相关的type添加常量:

编写person相关的action.js:

7.6.2.7 数据共享-编写Person组件的reducer

看一下如果写成 return prevState.unshift(data) ,真正的rens是什么。

image-20230818105610263

可以看到,返回的是1。

那么如果我这样做呢?

看一下效果,添加一条数据:

可以看到prevState的数据确实更改了,但是页面是没有渲染的。为什么呢?因为redux没有识别到,redux里面的reducer必须写成一个纯函数,prevState添加data后,它在内存中的地址并没有发生变化,而redux进行的是浅比较,既然return出去的数据和之前的prevState内存地址一样,那么页面就不会更新。

return [data,...prevState]是一个新数组。

7.6.2.8 数据共享-完成数据共享

7.6.2.6和7.6.2.7的步骤和之前的实现count的案例是相同的,下面就是区别了。

1、在store中引入person相关的reducer,并注册。

redux管理多个共享状态,怎么注册多个reducer呢?redux要管理多个状态,最佳的存储状态的方式就是用一个对象来管理,需要用combineReducers来合并这些reducer成为一个对象。

问题:这个合并到底是做了什么呢?按照老师的说法,是合并了状态。但好像UI组件使用操作状态的方法都没有变,只有取值的时候变化了,为什么呢?这个问题必须理解清楚,否则就会懵懵懂懂,等到用的时候就会非常迟疑,最后就不会用了。

仔细看redux的原理图,store和reducer的通信过程是什么样的?store交给reducer的是(prevState,action),reducer返回给store的是newState。那么多个reducer返回给store的newState,该怎么管理呢?那就需要一个对象来管理了,要显式的指定key,并赋值value(newState)。

说实话,store交给reducer参数的过程,我们是不用管的,我们要管的是通过dispatch将action传递给store,store帮助我们处理。那么这个dispatch传递action给store的过程,需要变化吗?不需要。为什么?

https://www.redux.org.cn/docs/basics/DataFlow.html

image-20230818092333174

因为combineReducers()之后的总reducer会识别到具体的action,并给具体的reducer传参数。

所以就回答了上面的问题“UI组件使用操作状态的方法没有变”。但是使用状态的方法是变化了的,看上面的图,返回的是一个合并成的state树(也就是一个对象),需要通过key来取值。

2、编写person相关的容器组件,并将UI组件和redux联系起来,实现功能。

3、改写count相关的文件,实现显示persons列表。

 

7.6.3. 相关API

  1. Provider:让所有组件都可以得到store里面的state数据
  1. connect:用于包装 UI 组件生成容器组件
  1. mapStateToProps:将外部的数据(即state对象)转换为UI组件的标签属性
  1. mapDispatchToProps:将分发action的函数转换为UI组件的标签属性

7.7. 使用redux调试工具

如果项目的redux很复杂,那么可以浏览器的redux调试工具来查看状态的变化。

7.7.1. 安装chrome浏览器插件

image-20211118163236139

7.7.2. 下载工具依赖包

image-20230818214047036

image-20230818214256925

image-20230818214430616

image-20230818214827528

image-20230818215002218

7.8. 纯函数和高阶函数

7.8.1. 纯函数

  1. 一类特别的函数: 只要是同样的输入(实参),必定得到同样的输出(返回)

  2. 必须遵守以下一些约束

    1)不得改写参数数据

    2)不会产生任何副作用,例如网络请求,输入和输出设备

    3)不能调用Date.now()或者Math.random()等不纯的方法

  3. redux的reducer函数必须是一个纯函数

"只要是同样的输入(实参),必定得到同样的输出(返回)"怎么理解?我好像没有写过参数一样,输出结果不一样的函数啊。

 

7.8.2. 高阶函数

  1. 理解: 一类特别的函数

    1)情况1: 参数是函数

    2)情况2: 返回是函数

  2. 常见的高阶函数:

    1)定时器设置函数

    2)数组的forEach()/map()/filter()/reduce()/find()/bind()

    3)promise

    4)react-redux中的connect函数

  3. 作用: 能实现更加动态, 更加可扩展的功能

7.9 在项目中使用redux的最终版

进行下面的优化:

1、不合理的命名进行修改,he、rens、jia、jian、jiaAsync都要改成英文

2、有些命名太长了,比如说createIncrementAction,可以直接改成increment。等等。

3、在redux/reducers文件夹里面新建一个index.js,在这里使用combineReducers合并reducer,并暴露一个合并的reducer给store使用。

4、利用对象的属性名如果和值的变量名相同,可以简写的特性,对项目中使用到对象的地方进行改写。比如说容器组件里面connect()()第一次调用的时候,第二个参数传的对象,可以这样改写:

7.10 项目打包

执行:npm run build,项目中生成了一个build文件夹,就可以放到tomcat、NGINX等服务器上运行了。

提示:react官方推荐了一个可以在本地创建服务器的库serve,安装:npm install -g serve,如果处于打包文件夹里面的命令行,那么直接执行serve就可以启动服务器;如果处于打包文件夹的同层级,执行serve build就可以启动服务器。

image-20230818222917837

当然,这个库只是方便查看项目是否正常的,真正上线还是要用专业的服务器。