💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
&emsp;&emsp;Redux是一个可预测的状态容器,不但融合了函数式编程思想,还严格遵循了单向数据流的理念。Redux继承了Flux的架构思想,并在此基础上进行了精简、优化和扩展,力求用最少的API完成最主要的功能,它的核心代码短小而精悍,压缩后只有几KB。Redux约定了一系列的规范,并且标准化了状态(即数据)的更新步骤,从而让不断变化、快速增长的大型前端应用中的状态有迹可循,既利于问题的重现,也便于新需求的整合。注意,Redux是一个独立的库,可与React、Ember或jQuery等其它库搭配使用。 &emsp;&emsp;在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();   //读取状态 ~~~ &emsp;&emsp;通过上面的代码可知,Action是一个普通的JavaScript对象,Reducer是一个纯函数,Store是一个通过createStore()函数得到的对象,如果要触发状态的更新,那么需要调用它的dispatch()方法。先对Redux有个初步的感性认识,然后在接下来的章节中,将围绕这段代码展开具体的分析。 ## 一、三大原则 &emsp;&emsp;只有遵守Redux所设计的三大原则,才能让状态变得可预测。 &emsp;&emsp;(1)单一数据源(Single source of truth)。 &emsp;&emsp;前端应用中的所有状态会组成一个树形的JavaScript对象,被保存到一个Store中。这样不但能避免数据冗余,还易于调试,并且便于监控任意时刻的状态,从而减少出错概率。不仅如此,过去难以达成的功能(例如即时保存、撤销重做等),现在实现起来也变得易如反掌了。在应用的任意位置,可通过Store的getState()方法读取到当前的状态。 &emsp;&emsp;(2)保持状态只读(State is read-only)。 &emsp;&emsp;若要改变Redux中的状态,得先派发一个Action对象,然后再由Reducer函数创建一个新的状态对象返回给Redux,以此保证状态的只读,从而让状态管理能够井然有序的进行。 &emsp;&emsp;(3)状态的改变由纯函数完成(Changes are made with pure functions)。 &emsp;&emsp;这里所说的纯函数是指Reducer,它没有副作用(即输出可预测),其功能就是接收Action并处理状态的变更,通过Reducer函数使得历史状态变得可追踪。 ## 二、主要组成 &emsp;&emsp;Redux主要由三部分组成:Action、Reducer和Store,本节将会对它们依次进行讲解。 **1)Action** &emsp;&emsp;由开发者定义的Action本质上就是一个普通的JavaScript对象,Redux约定该对象必须包含一个字符串类型的type属性,其值是一个常量,用来描述动作意图。Action的结构可自定义,尽量包含与状态变更有关的信息,以下面递增数值的Action对象为例,除了必需的type属性之外,还额外附带了一个表示增量的step属性。 ~~~js { type: "ADD", step: 1 } ~~~ &emsp;&emsp;如果项目规模越来越大,那么可以考虑为Action加个唯一号标识或者分散到不同的文件中。 &emsp;&emsp;通常会用Action创建函数(Action Creator)生成Action对象(即返回一个Action对象),因为函数有更好的可控性、移植性和可测试性,下面是一个简易的Action创建函数。 ~~~js function add() { return { type: "ADD", step: 1 }; } ~~~ **2)Reducer** &emsp;&emsp;Reducer函数对状态只计算不存储,开发者可根据当前业务对其进行自定义。此函数能接收2个参数:previousState和action,前者表示上一个状态(即当前应用的状态),后者是一个被派发的Action对象,函数体中的返回值是根据这两个参数生成的一个处理过的新状态。 &emsp;&emsp;Redux在首次执行时,由于初始状态为undefined,因此可以为previousState设置初始值,例如像下面这样使用ES6默认参数的语法。 ~~~js function caculate(previousState = {digit: 0}, action) { let state = Object.assign({}, previousState); //省略更新逻辑 return state; } ~~~ &emsp;&emsp;在编写Reducer函数时,有三点需要注意: &emsp;&emsp;(1)遵守纯函数的规范,例如不修改参数、不执行有副作用的函数等。 &emsp;&emsp;(2)在函数中可以先用Object.assign()创建一个状态对象的副本,随后就只修改这个新对象,注意,方法的第一个参数要像上面这样传一个空对象。 &emsp;&emsp;(3)在发生异常情况(例如无法识别传入的Action对象),返回原来的状态。 &emsp;&emsp;当业务变得复杂时,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}); ~~~ &emsp;&emsp;combineReducers()会先执行一次这两个函数,也就是说reducers()函数所要计算的初始状态不再是undefined,而是下面这个对象。注意,{add, minus}用到了ES6新增的简洁属性语法。 ~~~js { add: { digit: 0 }, minus: { number: 0 } } ~~~ **3)Store** &emsp;&emsp;Store为Action和Reducer架起了一座沟通的桥梁,它是Redux中的一个对象,发挥了容器的作用,保存着应用的状态,包含4个方法: &emsp;&emsp;(1)getState():获取当前状态。 &emsp;&emsp;(2)dispatch(action):派发一个Action对象,引起状态的修改。 &emsp;&emsp;(3)subscribe(listener):注册状态更新的监听器,其返回值可以注销该监听器。 &emsp;&emsp;(4)replaceReducer(nextReducer):更新Store中的Reducer函数,在实现Redux热加载时可能会用到。 &emsp;&emsp;在Redux应用中,只会包含一个Store,由createStore()函数创建,它的第一个参数是Reducer()函数,第二个参数是可选的初始状态,如下代码所示,为其传入了开篇的caculate()函数和一个包含digit属性的对象。 ~~~js let store = createStore(caculate, {digit: 1}); ~~~ &emsp;&emsp;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 &emsp;&emsp;虽然Redux和React可以单独使用(即没有直接关联),但是将两者搭配起来能发挥更大的作用。React应用的规模一旦上去,那么对状态的维护就变得愈加棘手,而在引入Redux后就能规范状态的变化,从而扭转这种窘境。Redux官方提供了一个用于绑定React的库:react-redux,它包含一个connect()函数和一个Provider组件,能很方便的将Redux的特性融合到React组件中。 **1)容器组件和展示组件** &emsp;&emsp;由于react-redux库是基于容器组件和展示组件相分离的开发思想而设计的,因此在正式讲解react-redux之前,需要先理清这两类组件的概念。 &emsp;&emsp;容器组件(Container Component),也叫智能组件(Smart Component),由react-redux库生成,负责应用逻辑和源数据的处理,为展示组件传递必要的props,可与Redux配合使用,不仅能监听Redux的状态变化,还能向Redux派发Action。 &emsp;&emsp;展示组件(Presentational Component),也叫木偶组件(Dumb Component),由开发者定义,负责渲染界面,接收从容器组件传来的props,可通过props中的回调函数同步源数据的变更。 &emsp;&emsp;容器组件和展示组件是根据职责划分的,两者可互相嵌套,并且它们内部都可以包含或省略状态,一般容器组件是一个有状态的类,而展示组件是一个无状态的函数。 **2)connect()** &emsp;&emsp;react-redux提供了一个柯里化函数:connect(),它包含4个可选的参数(如下代码所示),用于连接React组件与Redux的Store(即让展示组件关联Redux),生成一个容器组件。 ~~~js connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) ~~~ &emsp;&emsp;在使用connect()时会有两次函数执行,如下代码所示,第一次是获取要使用的保存在Store中的状态,connect()函数的返回结果是一个函数;第二次是把一个展示组件Dumb传到刚刚返回的函数中,继而将该组件装饰成一个容器组件Smart。 ~~~js const Smart = connect()(Dumb); ~~~ &emsp;&emsp;接下来会着重讲解函数的前两个参数:mapStateToProps和mapDispatchToProps,另外两个参数(mergeProps和options)可以参考官方文档的说明。 **3)mapStateToProps** &emsp;&emsp;这是一个包含2个参数的函数(如下代码所示),其作用是从Redux的Store中提取出所需的状态并计算成展示组件的props。如果connect()函数省略这个参数,那么展示组件将无法监听Store的变化。 ~~~js mapStateToProps(state, [ownProps]) ~~~ &emsp;&emsp;第一个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") ); ~~~ &emsp;&emsp;Btn是一个无状态的展示组件,Store中保存的初始状态不是undefined,容器组件Smart接收到了一个txt属性,在mapStateToProps()函数中打印出了两个参数的值。 &emsp;&emsp;当Store中的状态发生变化或组件接收到新的props时,mapStateToProps()函数就会被自动调用。 **4)mapDispatchToProps** &emsp;&emsp;它既可以是一个对象,也可以是一个函数,如下代码所示。其作用是绑定Action创建函数与Store实例所提供的dispatch()方法,再将绑好的方法映射到展示组件的props中。 ~~~js function add() { //Action创建函数 return {type: "ADD"}; } var mapDispatchToProps = { add };      //对象 var mapDispatchToProps = (dispatch, ownProps) => { //函数 return {add: bindActionCreators(add, dispatch)}; } ~~~ &emsp;&emsp;当mapDispatchToProps是一个对象时,其包含的方法会作为Action创建函数,自动传递给Redux内置的bindActionCreators()方法,生成的新方法会合并到props中,属性名沿用之前的方法名。 &emsp;&emsp;当mapDispatchToProps是一个函数时,会包含2个参数,第一个dispatch参数就是Store实例的dispatch()方法;第二个ownProps参数的含义与mapStateToProps中的相同,并且也是可选的。函数的返回值是一个由方法组成的对象(会合并到props中),在方法中会派发一个Action对象,而利用bindActionCreators()方法就能简化派发流程,其源码如下所示。 ~~~js function bindActionCreator(actionCreator, dispatch) { return function () { return dispatch(actionCreator.apply(this, arguments)); }; } ~~~ &emsp;&emsp;展示组件能通过读取props的属性来调用传递过来的方法,例如在Btn组件的点击事件中执行props.add(),触发状态的更新,如下所示。 ~~~js function Btn(props) { return <button onClick={props.add}>{props.txt}</button>; } ~~~ &emsp;&emsp;通过上面的分析可知,mapStateToProps负责展示组件的输入,即将所需的应用状态映射到props中;mapDispatchToProps负责展示组件的输出,即将需要执行的更新操作映射到props中。 **5)Provider** &emsp;&emsp;react-redux提供了Provider组件,它能将Store保存在自己的Context(在[第9篇](https://www.cnblogs.com/strick/p/10668965.html)做过讲解)中。如果要正确使用容器组件,那么得让其成为Provider组件的后代,并且只有这样才能接收到传递过来的Store。Provider组件常见的用法如下所示。 ~~~html <Provider store={store}> <Smart /> </Provider> ~~~ &emsp;&emsp;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 等。