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 --save 或 yarn 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可以有效的组织订阅者发生刷新