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

 

 posted on 2019-07-29 09:28  咖啡机(K.F.J)  阅读(433)  评论(0编辑  收藏  举报