zoukankan      html  css  js  c++  java
  • React躬行记(11)——Redux基础

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

  • 相关阅读:
    Codeforces 220B
    NYOJ1367 物流配送
    BZOJ2561 最小生成树
    BZOJ3894 文理分科
    BZOJ3245 最快路线
    BZOJ1497 最大获利
    Codeforces Round #555 (Div. 3)
    MySQL更改默认的root账户密码
    sublime3常用环境配置
    2.9 logistic回归中的梯度下降法(非常重要,一定要重点理解)
  • 原文地址:https://www.cnblogs.com/strick/p/10775503.html
Copyright © 2011-2022 走看看