Redux相关基础总结

453 阅读19分钟

javaScript纯函数

函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念。

纯函数的维基百科定义:

在程序设计中,若一个函数符合一下条件,那么这个函数被称为纯函数:

  • 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

上面的定义会过于的晦涩,所以我简单总结一下:

  • 确定的输入,一定会产生确定的输出;
  • 函数在执行过程中,不能产生副作用;

那么我们来看几个函数是否是纯函数:

  • 案例一:

下面的函数是一个纯函数;它的输出是依赖我们的输入内容,并且中间没有产生任何副作用;

  • 案例二:

add函数不是一个纯函数;函数依赖一个外部的变量,变量发生改变时,会影响:确定的输入,产生确定的输出;能否改进成纯函数呢? const foo = 5; 即可

当然纯函数还有很多的变种,但是我们只需要理解它的核心就可以了。为什么纯函数在函数式编程中非常重要呢?

  • 因为你可以安心的写和安心的用;
  • 你在写的时候保证了函数的纯度,只是但是实现自己的业务逻辑即可,不需要关心传入的内容或者依赖其他的外部变量;
  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:React非常灵活,但它也有一个严格的规则:所有React组件都必须像纯函数一样保护它们的props不被更改

为什么需要redux

JavaScript开发的应用程序,已经变得越来越复杂了:

  • JavaScript需要管理的状态越来越多,越来越复杂;
  • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;

管理不断变化的state是非常困难的:

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
  • 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;

React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:

  • 无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;
  • React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定;

Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)

Redux的核心理念

store

Redux的核心理念非常简单。比如我们有一个朋友列表需要管理:

  • 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
  • 比如页面的某处通过products.push的方式增加了一条数据;
  • 比如另一个页面通过products[0].age = 25修改了一条数据;

整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化;

action

Redux要求我们通过action来更新数据:

  • 所有数据的变化,必须通过派发(dispatch)action来更新;
  • action是一个普通的JavaScript对象,用来描述这次更新的type和content;

比如下面就是几个更新friends的action:

  • 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;
  • 当然,目前我们的action是固定的对象,真实应用中,我们会通过函数来定义,返回一个action;

reducer

但是如何将state和action联系在一起呢?答案就是reducer

  • reducer是一个纯函数;
  • reducer做的事情就是将传入的state和action结合起来生成一个新的state;

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都应该是纯函数,不能产生任何的副作用;

Redux的使用过程

首先安装redux,安装命令: npm install redux --saveyarn add redux

1.创建一个对象,作为我们要保存的状态:

2.创建Store来存储这个state

  • 创建store时必须创建reducer;
  • 通过 store.getState 来获取当前的state

3.通过action来修改state

  • 通过dispatch来派发action;
  • 通常action中都会有type属性,也可以携带其他的数据;

4.修改reducer中的处理代码

  • reducer是一个纯函数,不需要直接修改state;

5.可以在派发action之前,监听store的变化:

Redux结构划分

如果将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护。

接下来,对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。

constants.js

actionCreator.js

reducer.js

index.js

Redux官方图

react-redux使用

需要强调一下,redux和react没有直接的关系,尽管这样说,redux依然是和React或者Deku的库结合的更好,因为它们是通过state函数来描述界面的状态,Redux可以发射状态的更新,让他们作出响应。redux官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。

安装react-redux:npm install react-redux

react-redux提供两个核心的api: 

  • Provider: 提供store
  • connect: 用于连接容器组件和展示组件

provider

根据单一store原则 ,一般只会出现在整个应用程序的最顶层。首先,需要在react-redux中解构出Provider,在App根组件上嵌套一层Provider,并传入store,store为redux.createStore创建出来的仓库,里面存储着全局的数据。

connect

语法格式为 

connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)(component)`

 一般来说只会用到前面两个,它的作用是:

  •  把store.getState()的状态转化为展示组件的props
  •  把actionCreators转化为展示组件props上的方法 

特别强调: 官网上的第二个参数为mapDispatchToProps, 实际上就是actionCreators 只要上层中有Provider组件并且提供了store, 那么,子孙级别的任何组件,要想使用store里的状态,都可以通过connect方法进行连接。如果只是想连接actionCreators,可以第一个参数传递为null

组件中异步操作

真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:

事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;

但是在redux中如何可以进行异步的操作呢?答案就是使用中间件(Middleware)。Middleware可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie解析、日志记录、文件压缩等操作;

理解中间件

redux也引入了中间件(Middleware)的概念:

  • 中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;
  • 比如日志记录、调用异步接口、添加代码调试功能等等;

我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:这里官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk;

redux-thunk是如何做到让我们可以发送异步的请求呢?

默认情况下的dispatch(action),action需要是一个JavaScript的对象;redux-thunk可以让dispatch(action函数),action可以是一个函数;

  • 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
  • dispatch函数用于我们之后再次派发action;
  • getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;

如何使用redux-thunk

1.安装redux-thunk

安装命令:yarn add redux-thunk

2.在创建store时传入应用了middleware的enhance函数

通过applyMiddleware来结合多个Middleware, 返回一个enhancer;

将enhancer作为第二个参数传入到createStore中;

3.定义返回一个函数的action:

注意:这里不是返回一个对象了,而是一个函数;该函数在dispatch之后会被执行;

Reducer代码拆分

我们先来理解一下,为什么这个函数叫reducer?

假如当前这个reducer既有处理counter的代码,又有处理home页面的数据;后续counter相关的状态或home相关的状态会进一步变得更加复杂;我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。

因此,我们可以对reducer进行拆分:

  • 我们先抽取一个对counter处理的reducer;
  • 再抽取一个对home处理的reducer;
  • 将它们合并起来;

目前我们已经将不同的状态处理拆分到不同的reducer中,我们来思考:

虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;

另外关于reducer中用到的constant、action等我们也依然是在同一个文件中;目录结构:

combineReducers函数

目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象。

事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:

那么combineReducers是如何实现的呢?

事实上,它也是讲我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新