Redux是一个可预测的状态容器,不但融合了函数式编程思想,还严格遵循了单向数据流的理念。Redux继承了Flux的架构思想,并在此基础上进行了精简、优化和扩展,力求用最少的API完成最主要的功能,它的核心代码短小而精悍,压缩后只有几KB。Redux约定了一系列的规范,并且标准化了状态(即数据)的更新步骤,从而让不断变化、快速增长的大型前端应用中的状态有迹可循,既利于问题的重现,也便于新需求的整合。注意,Redux是一个独立的库,可与React、Ember或jQuery等其它库搭配使用。

  在Redux中,状态是不能直接被修改的,而是通过Action、Reducer和Store三部分协作完成的。具体的运作流程可简单的概括为三步,首先由Action说明要执行的动作,然后让Reducer设计状态的运算逻辑,最后通过Store将Action和Reducer关联并触发状态的更新,下面用代码演示这个流程。

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属性。

{ type: "ADD", step: 1 }

  如果项目规模越来越大,那么可以考虑为Action加个唯一号标识或者分散到不同的文件中。

  通常会用Action创建函数(Action Creator)生成Action对象(即返回一个Action对象),因为函数有更好的可控性、移植性和可测试性,下面是一个简易的Action创建函数。

function add() {
return { type: "ADD", step: 1 };
}

2)Reducer

  Reducer函数对状态只计算不存储,开发者可根据当前业务对其进行自定义。此函数能接收2个参数:previousState和action,前者表示上一个状态(即当前应用的状态),后者是一个被派发的Action对象,函数体中的返回值是根据这两个参数生成的一个处理过的新状态。

  Redux在首次执行时,由于初始状态为undefined,因此可以为previousState设置初始值,例如像下面这样使用ES6默认参数的语法。

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()函数。

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新增的简洁属性语法。

{ 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属性的对象。

let store = createStore(caculate, {digit: 1});

  caculate()函数会增加或减少状态对象的digit属性,其中增量或减量都是1。接下来为Store注册一个监听器(如下代码所示),当状态更新时,就会打印出最新的状态;而在注销监听器(即调用unsubscribe()函数)后,控制台就不会再有任何输出。

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),生成一个容器组件。

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

  在使用connect()时会有两次函数执行,如下代码所示,第一次是获取要使用的保存在Store中的状态,connect()函数的返回结果是一个函数;第二次是把一个展示组件Dumb传到刚刚返回的函数中,继而将该组件装饰成一个容器组件Smart。

const Smart = connect()(Dumb);

  接下来会着重讲解函数的前两个参数:mapStateToProps和mapDispatchToProps,另外两个参数(mergeProps和options)可以参考官方文档的说明。

3)mapStateToProps

  这是一个包含2个参数的函数(如下代码所示),其作用是从Redux的Store中提取出所需的状态并计算成展示组件的props。如果connect()函数省略这个参数,那么展示组件将无法监听Store的变化。

mapStateToProps(state, [ownProps])

  第一个state参数是Store中保存的状态,第二个可选的ownProps参数是传递给容器组件的props对象。在一般情况下,mapStateToProps()函数会返回一个对象,但当需要控制渲染性能时,可以返回一个函数。下面是一个简单的例子,还是沿用开篇的caculate()函数,Provider组件的功能将在后文中讲解。

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中。

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()方法就能简化派发流程,其源码如下所示。

function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(this, arguments));
};
}

  展示组件能通过读取props的属性来调用传递过来的方法,例如在Btn组件的点击事件中执行props.add(),触发状态的更新,如下所示。

function Btn(props) {
return <button onClick={props.add}>{props.txt}</button>;
}

  通过上面的分析可知,mapStateToProps负责展示组件的输入,即将所需的应用状态映射到props中;mapDispatchToProps负责展示组件的输出,即将需要执行的更新操作映射到props中。

5)Provider

  react-redux提供了Provider组件,它能将Store保存在自己的Context(在第9篇做过讲解)中。如果要正确使用容器组件,那么得让其成为Provider组件的后代,并且只有这样才能接收到传递过来的Store。Provider组件常见的用法如下所示。

<Provider store={store}>
<Smart />
</Provider>

  Provider组件位于顶层的位置,它会接收一个store属性,属性值就是createStore()函数的返回值,Smart是一个容器组件,被嵌套在Provider组件中。

React躬行记(11)——Redux基础的更多相关文章

  1. React躬行记(12)——Redux中间件

    Redux的中间件(Middleware)遵循了即插即用的设计思想,出现在Action到达Reducer之前(如图10所示)的位置.中间件是一个固定模式的独立函数,当把多个中间件像管道那样串联在一起时 ...

  2. React躬行记(13)——React Router

    在网络工程中,路由能保证信息从源地址传输到正确地目的地址,避免在互联网中迷失方向.而前端应用中的路由,其功能与之类似,也是保证信息的准确性,只不过来源变成URL,目的地变成HTML页面. 在传统的前端 ...

  3. React躬行记(8)——样式

    由于React推崇组件模式,因此会要求HTML.CSS和JavaScript混合在一起,虽然这与过去的关注点分离正好相反,但是更有利于组件之间的隔离.React已将HTML用JSX封装,而对CSS只进 ...

  4. React躬行记(5)——React和DOM

    React实现了一套与浏览器无关的DOM系统,包括元素渲染.节点查询.事件处理等机制. 一.ReactDOM 自React v0.14开始,官方将与DOM相关的操作从React中剥离,组成单独的rea ...

  5. React躬行记(6)——事件

    React在原生事件的基础上,重新设计了一套跨浏览器的合成事件(SyntheticEvent),在事件传播.注册方式.事件对象等多个方面都做了特别的处理. 一.注册事件 合成事件采用声明式的注册方式, ...

  6. React躬行记(9)——组件通信

    根据组件之间的嵌套关系(即层级关系)可分为4种通信方式:父子.兄弟.跨级和无级. 一.父子通信 在React中,数据是自顶向下单向流动的,而父组件通过props向子组件传递需要的信息是组件之间最常见的 ...

  7. React躬行记(3)——组件

    组件(Component)由若干个React元素组成,包含属性.状态和生命周期等部分,满足独立.可复用.高内聚和低耦合等设计原则,每个React应用程序都是由一个个的组件搭建而成,即组成React应用 ...

  8. React躬行记(10)——高阶组件

    高阶组件(High Order Component,简称HOC)不是一个真的组件,而是一个没有副作用的纯函数,以组件作为参数,返回一个功能增强的新组件,在很多第三方库(例如Redux.Relay等)中 ...

  9. React躬行记(2)——JSX

    JSX既不是字符串,也不是HTML,而是一种类似XML,用于描述用户界面的JavaScript扩展语法,如下代码所示.在使用JSX时,为了避免自动插入分号时出现问题,推荐在其最外层用圆括号包裹,并且必 ...

随机推荐

  1. 关于Git 的管理凭据操作

    1.桌面-->2.我的电脑-->3.右击选择属性-->4.控制面板主页-->5.在用户账户和家庭安全下,选择添加或删除用户账户-->转到“主用户账户”页面-->6. ...

  2. spring_three

    转账案例 坐标: ; } } 创建增强类Logger.java /** * 用于记录日志的工具类,它里面提供了公共的代码 */ @Component("logger") @Aspe ...

  3. 系统学习 Java IO (八)----装饰流 FilterInputStream/FilterOutputStream

    目录:系统学习 Java IO---- 目录,概览 这两个流的作用是:"封装其它的输入流,并为它们提供额外的功能" 他们的直接子类有: BufferedInputStream 的作 ...

  4. 第三章: Expressions and Flow Control

    第三章: Expressions and Flow Control一:局部变量和实例变量定义变量是指设定变量的数据类型和变量的名字,Java语言要求变量遵循先定义,再初始化,然后使用的规则.作用域:指 ...

  5. java8 异步api、循环、日期

    java8 异步api.循环.日期 转载请注明出处:https://www.cnblogs.com/funnyzpc/p/10801470.html 异步api 对于多任务耗时的业务场景,一般我们会用 ...

  6. centos7.3 格式化和挂载数据盘

    本文使用 fdisk 命令对小于 2 TiB 的数据盘执行分区操作. 1.  运行 fdisk -l 命令查看实例是否有数据盘 2.  创建一个单分区数据盘,依次执行以下命令: 运行 fdisk /d ...

  7. POJ 3183:Stump Removal(模拟)

    http://poj.org/problem?id=3183 题意:有n个树桩,分别有一个高度h[i],要用Bomb把树桩都炸掉,如果炸的位置的两边树桩高度小于Bomb炸的树桩高度,那么小于树桩高度的 ...

  8. 【RabbitMQ】一文带你搞定RabbitMQ死信队列

    本文口味:爆炒鱿鱼   预计阅读:15分钟 一.说明 RabbitMQ是流行的开源消息队列系统,使用erlang语言开发,由于其社区活跃度高,维护更新较快,性能稳定,深得很多企业的欢心(当然,也包括我 ...

  9. python基础认识(一)

    这些日子以来,新闻铺天盖地的都是人工智能,那么借着这股潮流,python也随之火起来了,现在的python不仅仅可以进行人工智能领域的开发.还可以进行web.爬虫等领域的运用.因此,我认为作为一个紧跟 ...

  10. 两个域名同时访问一个tomcat下的两个项目

    两个域名,分别映射一个TOMCAT底下,两个应用. 分三个步骤完成. 1.域名与IP的解析,此步骤在万网等机构完成. 2.APACHE的httpd.conf的配置 <VirtualHost *: ...