Redux是一个可预测的状态容器,不但融合了函数式编程思想,还严格遵循了单向数据流的理念。Redux继承了Flux的架构思想,并在此基础上进行了精简、优化和扩展,力求用最少的API完成最主要的功能,它的核心代码短小而精悍,压缩后只有几KB。Redux约定了一系列的规范,并且标准化了状态(即数据)的更新步骤,从而让不断变化、快速增长的大型前端应用中的状态有迹可循,既利于问题的重现,也便于新需求的整合。注意,Redux是一个独立的库,可与React、Ember或jQuery等其它库搭配使用。
  在Redux中,状态是不能直接被修改的,而是通过Action、Reducer和Store三部分协作完成的。具体的运作流程可简单的概括为三步,首先由Action说明要执行的动作,然后让Reducer设计状态的运算逻辑,最后通过Store将Action和Reducer关联并触发状态的更新,下面用代码演示这个流程。
~~~js
function caculate(previousState = {digit: 0}, action) { //Reducer
let state = Object.assign({}, previousState);
switch (action.type) {
case "ADD":
state.digit += 1;
break;
case "MINUS":
state.digit -= 1;
}
return state;
}
let store = createStore(caculate); //Store
let action = { type: "ADD" }; //Action
store.dispatch(action); //触发更新
store.getState(); //读取状态
~~~
  通过上面的代码可知,Action是一个普通的JavaScript对象,Reducer是一个纯函数,Store是一个通过createStore()函数得到的对象,如果要触发状态的更新,那么需要调用它的dispatch()方法。先对Redux有个初步的感性认识,然后在接下来的章节中,将围绕这段代码展开具体的分析。
## 一、三大原则
  只有遵守Redux所设计的三大原则,才能让状态变得可预测。
  (1)单一数据源(Single source of truth)。
  前端应用中的所有状态会组成一个树形的JavaScript对象,被保存到一个Store中。这样不但能避免数据冗余,还易于调试,并且便于监控任意时刻的状态,从而减少出错概率。不仅如此,过去难以达成的功能(例如即时保存、撤销重做等),现在实现起来也变得易如反掌了。在应用的任意位置,可通过Store的getState()方法读取到当前的状态。
  (2)保持状态只读(State is read-only)。
  若要改变Redux中的状态,得先派发一个Action对象,然后再由Reducer函数创建一个新的状态对象返回给Redux,以此保证状态的只读,从而让状态管理能够井然有序的进行。
  (3)状态的改变由纯函数完成(Changes are made with pure functions)。
  这里所说的纯函数是指Reducer,它没有副作用(即输出可预测),其功能就是接收Action并处理状态的变更,通过Reducer函数使得历史状态变得可追踪。
## 二、主要组成
  Redux主要由三部分组成:Action、Reducer和Store,本节将会对它们依次进行讲解。
**1)Action**
  由开发者定义的Action本质上就是一个普通的JavaScript对象,Redux约定该对象必须包含一个字符串类型的type属性,其值是一个常量,用来描述动作意图。Action的结构可自定义,尽量包含与状态变更有关的信息,以下面递增数值的Action对象为例,除了必需的type属性之外,还额外附带了一个表示增量的step属性。
~~~js
{ type: "ADD", step: 1 }
~~~
  如果项目规模越来越大,那么可以考虑为Action加个唯一号标识或者分散到不同的文件中。
  通常会用Action创建函数(Action Creator)生成Action对象(即返回一个Action对象),因为函数有更好的可控性、移植性和可测试性,下面是一个简易的Action创建函数。
~~~js
function add() {
return { type: "ADD", step: 1 };
}
~~~
**2)Reducer**
  Reducer函数对状态只计算不存储,开发者可根据当前业务对其进行自定义。此函数能接收2个参数:previousState和action,前者表示上一个状态(即当前应用的状态),后者是一个被派发的Action对象,函数体中的返回值是根据这两个参数生成的一个处理过的新状态。
  Redux在首次执行时,由于初始状态为undefined,因此可以为previousState设置初始值,例如像下面这样使用ES6默认参数的语法。
~~~js
function caculate(previousState = {digit: 0}, action) {
let state = Object.assign({}, previousState);
//省略更新逻辑
return state;
}
~~~
  在编写Reducer函数时,有三点需要注意:
  (1)遵守纯函数的规范,例如不修改参数、不执行有副作用的函数等。
  (2)在函数中可以先用Object.assign()创建一个状态对象的副本,随后就只修改这个新对象,注意,方法的第一个参数要像上面这样传一个空对象。
  (3)在发生异常情况(例如无法识别传入的Action对象),返回原来的状态。
  当业务变得复杂时,Reducer函数中处理状态的逻辑也会随之变得异常庞大。此时,就可以采用分而治之的设计思想,将其拆分成一个个小型的独立子函数,而这些Reducer函数各自只负责维护一部分状态。如果需要将它们合并成一个完整的Reducer函数,那么可以使用Redux提供的combineReducers()函数。该函数会接收一个由拆分的Reducer函数组成的对象,并且能将它们的结果合并成一个完整的状态对象。下面是一个用法示例,先将之前的caculate()函数拆分成add()和minus()两个函数,再作为参数传给combineReducers()函数。
~~~js
function add(previousState, action) {
let state = Object.assign({}, previousState);
state.digit = "digit" in state ? (state.digit + 1) : 0;
return state;
}
function minus(previousState, action) {
let state = Object.assign({}, previousState);
state.number = "number" in state ? (state.number - 1) : 0;
return state;
}
let reducers = combineReducers({add, minus});
~~~
  combineReducers()会先执行一次这两个函数,也就是说reducers()函数所要计算的初始状态不再是undefined,而是下面这个对象。注意,{add, minus}用到了ES6新增的简洁属性语法。
~~~js
{ add: { digit: 0 }, minus: { number: 0 } }
~~~
**3)Store**
  Store为Action和Reducer架起了一座沟通的桥梁,它是Redux中的一个对象,发挥了容器的作用,保存着应用的状态,包含4个方法:
  (1)getState():获取当前状态。
  (2)dispatch(action):派发一个Action对象,引起状态的修改。
  (3)subscribe(listener):注册状态更新的监听器,其返回值可以注销该监听器。
  (4)replaceReducer(nextReducer):更新Store中的Reducer函数,在实现Redux热加载时可能会用到。
  在Redux应用中,只会包含一个Store,由createStore()函数创建,它的第一个参数是Reducer()函数,第二个参数是可选的初始状态,如下代码所示,为其传入了开篇的caculate()函数和一个包含digit属性的对象。
~~~js
let store = createStore(caculate, {digit: 1});
~~~
  caculate()函数会增加或减少状态对象的digit属性,其中增量或减量都是1。接下来为Store注册一个监听器(如下代码所示),当状态更新时,就会打印出最新的状态;而在注销监听器(即调用unsubscribe()函数)后,控制台就不会再有任何输出。
~~~js
let unsubscribe = store.subscribe(() => //注册监听器
console.log(store.getState())
);
store.dispatch({ type: "ADD" }); //{digit: 2}
store.dispatch({ type: "ADD" }); //{digit: 3}
unsubscribe(); //注销监听器
store.dispatch({ type: "MINUS" }); //没有输出
~~~
## 三、绑定React
  虽然Redux和React可以单独使用(即没有直接关联),但是将两者搭配起来能发挥更大的作用。React应用的规模一旦上去,那么对状态的维护就变得愈加棘手,而在引入Redux后就能规范状态的变化,从而扭转这种窘境。Redux官方提供了一个用于绑定React的库:react-redux,它包含一个connect()函数和一个Provider组件,能很方便的将Redux的特性融合到React组件中。
**1)容器组件和展示组件**
  由于react-redux库是基于容器组件和展示组件相分离的开发思想而设计的,因此在正式讲解react-redux之前,需要先理清这两类组件的概念。
  容器组件(Container Component),也叫智能组件(Smart Component),由react-redux库生成,负责应用逻辑和源数据的处理,为展示组件传递必要的props,可与Redux配合使用,不仅能监听Redux的状态变化,还能向Redux派发Action。
  展示组件(Presentational Component),也叫木偶组件(Dumb Component),由开发者定义,负责渲染界面,接收从容器组件传来的props,可通过props中的回调函数同步源数据的变更。
  容器组件和展示组件是根据职责划分的,两者可互相嵌套,并且它们内部都可以包含或省略状态,一般容器组件是一个有状态的类,而展示组件是一个无状态的函数。
**2)connect()**
  react-redux提供了一个柯里化函数:connect(),它包含4个可选的参数(如下代码所示),用于连接React组件与Redux的Store(即让展示组件关联Redux),生成一个容器组件。
~~~js
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
~~~
  在使用connect()时会有两次函数执行,如下代码所示,第一次是获取要使用的保存在Store中的状态,connect()函数的返回结果是一个函数;第二次是把一个展示组件Dumb传到刚刚返回的函数中,继而将该组件装饰成一个容器组件Smart。
~~~js
const Smart = connect()(Dumb);
~~~
  接下来会着重讲解函数的前两个参数:mapStateToProps和mapDispatchToProps,另外两个参数(mergeProps和options)可以参考官方文档的说明。
**3)mapStateToProps**
  这是一个包含2个参数的函数(如下代码所示),其作用是从Redux的Store中提取出所需的状态并计算成展示组件的props。如果connect()函数省略这个参数,那么展示组件将无法监听Store的变化。
~~~js
mapStateToProps(state, [ownProps])
~~~
  第一个state参数是Store中保存的状态,第二个可选的ownProps参数是传递给容器组件的props对象。在一般情况下,mapStateToProps()函数会返回一个对象,但当需要控制渲染性能时,可以返回一个函数。下面是一个简单的例子,还是沿用开篇的caculate()函数,Provider组件的功能将在后文中讲解。
~~~js
let store = createStore(caculate);
function Btn(props) { //展示组件
return <button>{props.txt}</button>;
}
function mapStateToProps(state, ownProps) {
console.log(state); //{digit: 0}
console.log(ownProps); //{txt: "提交"}
return state;
}
let Smart = connect(mapStateToProps)(Btn); //生成容器组件
ReactDOM.render(
<Provider store={store}>
<Smart txt="提交" />
</Provider>,
document.getElementById("container")
);
~~~
  Btn是一个无状态的展示组件,Store中保存的初始状态不是undefined,容器组件Smart接收到了一个txt属性,在mapStateToProps()函数中打印出了两个参数的值。
  当Store中的状态发生变化或组件接收到新的props时,mapStateToProps()函数就会被自动调用。
**4)mapDispatchToProps**
  它既可以是一个对象,也可以是一个函数,如下代码所示。其作用是绑定Action创建函数与Store实例所提供的dispatch()方法,再将绑好的方法映射到展示组件的props中。
~~~js
function add() { //Action创建函数
return {type: "ADD"};
}
var mapDispatchToProps = { add }; //对象
var mapDispatchToProps = (dispatch, ownProps) => { //函数
return {add: bindActionCreators(add, dispatch)};
}
~~~
  当mapDispatchToProps是一个对象时,其包含的方法会作为Action创建函数,自动传递给Redux内置的bindActionCreators()方法,生成的新方法会合并到props中,属性名沿用之前的方法名。
  当mapDispatchToProps是一个函数时,会包含2个参数,第一个dispatch参数就是Store实例的dispatch()方法;第二个ownProps参数的含义与mapStateToProps中的相同,并且也是可选的。函数的返回值是一个由方法组成的对象(会合并到props中),在方法中会派发一个Action对象,而利用bindActionCreators()方法就能简化派发流程,其源码如下所示。
~~~js
function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(this, arguments));
};
}
~~~
  展示组件能通过读取props的属性来调用传递过来的方法,例如在Btn组件的点击事件中执行props.add(),触发状态的更新,如下所示。
~~~js
function Btn(props) {
return <button onClick={props.add}>{props.txt}</button>;
}
~~~
  通过上面的分析可知,mapStateToProps负责展示组件的输入,即将所需的应用状态映射到props中;mapDispatchToProps负责展示组件的输出,即将需要执行的更新操作映射到props中。
**5)Provider**
  react-redux提供了Provider组件,它能将Store保存在自己的Context(在[第9篇](https://www.cnblogs.com/strick/p/10668965.html)做过讲解)中。如果要正确使用容器组件,那么得让其成为Provider组件的后代,并且只有这样才能接收到传递过来的Store。Provider组件常见的用法如下所示。
~~~html
<Provider store={store}>
<Smart />
</Provider>
~~~
  Provider组件位于顶层的位置,它会接收一个store属性,属性值就是createStore()函数的返回值,Smart是一个容器组件,被嵌套在Provider组件中。
*****
> 原文出处:
[博客园-React躬行记](https://www.cnblogs.com/strick/category/1455720.html)
[知乎专栏-React躬行记](https://zhuanlan.zhihu.com/pwreact)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎浏览。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020