Redux是Flux理念的一种实现。

关于Flux理念可以通过类比MVC模式来做简单理解。 MVC模式中,用户请求先到达Controller,由Controller调用Model获得数据,然后把数据交给View,按照这种模式,MVC应该也是一个controller->model->view的单向数据流,但是,在实际应用中,由于种种原因,往往会让view直接操作Model,随着应用的演进、逻辑变得越来约复杂,view与model之间的关系就会变得错综复杂、难以维护。在MVC中让View和Model直接对话就是灾难。 Flux理念可以简单看作是对MVC添加了更加严格的数据流限制。

Flux框架随React一同被Fackbook推出,但在Dan Abramov创建了Redux后,Redux已经替代了Flux。

基本原则

Flux的基本原则是“单向数据流”, Redux在此基础上强调三个基本原则:

  • 唯一数据源(Single Source of Truth),应用的状态数据应该只存储在唯一的一个Store上,它是树形结构,每个组件往往只是用树形对象上一部分的数据,而如何设计Store上状态的结构,就是Redux应用的核心问题。

  • 保持状态只读(State is read-only),不能直接修改store状态,必须要通过派发action对象的方式来进行。

  • 数据改变只能通过纯函数完成(Changesare made with pure functions)。

这个纯函数就是Reducer, Dan Abramov说过,Redux名字的含义就是Reducer+Flux。Reducer是一个很多语言都支持的函数,下面是js中数组的reduce函数的用法:

[1,2,3,4].reduce(function reducer(accumulation, item){
  return accumulation + item;
}, 0)

reduce函数的第一个参数是reducer, 这个函数把数组所有元素依次做“规约”,对每个元素都调用一次reducer,通过reducer函数完成规约所有元素的功能。

在Redux中,reducer的函数签名为:

reducer(state, action)

它根据state和action的值产生一个新的state对象返回,reducer是纯函数,只负责计算状态,不会去存储状态。

Redux的使用

使用create-react-app创建的模版中不包含redux依赖,首先需要执行npm install redux安装。

MVC中标准的单向数据流为controller->model->view的,对应地,在React配合Redux后,要触发view的更新,需要发出一个action,然后reducer根据action来更新store的状态,最后让view根据store中最新的数据来更新。

Action

Redux应用习惯上把action类型和action构造函数分成两个文件定义,两者的内容类似这样: ActionTypes.js

export const INCREMENT="increment";
export const DECREMENT="decrement";

Actions.js 这里的每个action构造函数都返回一个action对象

import * as ActionTypes from './ActionTypes.js';

export const increment = (counterCaption) => {
  return {
    type: ActionTypes.INCREMENT,
    counterCaption: counterCaption
  }
};

export const decrement = (counterCaption) => {
  return {
    type: ActionTypes.DECREMENT,
    counterCaption: counterCaption
  }
};

Store

Store.js举例

import { createStore } from 'redux';
import reducer from './Reducer.js';

const initValues = {
  'First': 0,
  'Second': 10,
  'Third': 20
}

const store = createStore(reducer, initValues);

export default store;

store的创建要调用Redux库提供的createStore函数:

  • 第一个参数为reducer,它负责更新更新状态
  • 第二个参数是状态的初始值
  • 还有第三个参数为Store Enhancer,后面再了解

确定Store状态,是设计好Redux应用的关键,其主要原则是:避免冗余的数据

Reducer

Reducer.js举例

import * as ActionTypes from './ActionTypes.js';

export default (state, action) => {
  const { counterCaption } = action;

  switch (action.type) {
    case ActionTypes.INCREMENT:
      return { ...state, [counterCaption]: state[counterCaption] + 1 };
    case ActionTypes.DECREMENT:
      return { ...state, [counterCaption]: state[counterCaption] - 1 };
    default:
      return state;
  }
}

Reducer的主要结构就是if-else或switch语句,根据action.type来执行对应的reduce操作。 ...state为扩展操作符语法,将state字段扩展开赋值给一个新的对象,再根据counterCaption修改对应的字段,这种简化写法等同于:

const newState = Object.assign({}, state);
newState[counterCaption]++;
return newState;

扩展操作符语法(spread operator)并不是ES6语法,但因其语法简单而被广泛使用,babel会负责解决兼容性问题。

reducer是纯函数,不会修改原有的state,而是操作新复制的state。

View

view代码举例:

import { Component } from 'react';
import PropTypes from 'prop-types';
import store from '../Store.js';
import * as Actions from '../Actions.js'

class Counter extends Component {
  constructor(props) {
    super(props);
    /* bind methods*/

    this.state = this.getOwnState();
  }

  getOwnState() {
    return {
      value: store.getState()[this.props.caption]
    }
  }

  onChange() {
    this.setState(this.getOwnState());
  }

  onClickIncrementButton() {
    store.dispatch(Actions.increment(this.props.caption));
  }

  onClickDecrementButton() {
    store.dispatch(Actions.decrement(this.props.caption));
  }

  componentDidMount() {
    store.subscribe(this.onChange);
  }

  componentWillUnmount() {
    store.unsubscribe(this.onChange);
  }

  render() {
    const { caption } = this.props;
    const value = this.state.value;

    return (
      <div>
        <button style={buttonStyle} onClick={this.onClickIncrementButton}>+</button>
        <button style={buttonStyle} onClick={this.onClickDecrementButton}>-</button>
        <span>{caption} count:{value}</span>
      </div>
    );
  }
}
  • store.getState()获取store上存储的所有状态,当前组件只需要获取自身相关的状态信息;
  • componentDidMount中,通过store.subscribe监听store状态的变化; store状态变化时,触发onChange,在这里会调用this.setState触发view更新;
  • 此外还要在componentWillUnmount中取消对store状态变化的监听;
  • 点击button时,通过store.dispatch分发一个action,这个action由Actions.js中的某个action构造函数产生。

容器组件和展示组件

从前文的Redux示例代码可以发现,在Redux框架下,一个React组件基本上就是要完成以下两个功能:

  • 与store相关的职责:
    • 读取并监听Store状态的改变;
    • 当Store状态变化时,更新组件状态,驱动组件重新渲染;
    • 当需要更新Store状态时,派发action对象;
  • 根据当前props和state,渲染出用户界面。

于是考虑将组件一分为二:

  • 容器组件:承担store相关职责
  • 展示组件:只关注页面渲染

展示组件没有状态,是一个纯函数,只需要根据props来渲染,不需要包含state;

组件Context

但是如果每个组件都拆分为容器组件和展示组件后,由于容器组件要与store打交道,那么每个容器组件都需要导入store,最好能只在顶层的组件导入一次,子组件就都可以用了,不过子组件使用时不是使用props层层传递,而是借助React提供的Context(上下文环境)。

要使用Context需要上下级组件之间的配合,首先来看顶层组件。 在此创建一个Provider组件作为一个通用的Context提供者

import { Component } from 'react';
import PropTypes from 'prop-types';

class Provider extends Component {
  getChildContext() {
    return {
      store: this.props.store
    };
  }

  render() {
    return this.props.children;
  }
}

Provider.childContextTypes = {
  store: PropTypes.object
}

Provider.propTypes = {
  store: PropTypes.object.isRequired
}

export default Provider;

这个组件作为父组件使用:

···
import store from './Store.js';
import Provider from './Provider.js';

ReactDOM.render(
  <Provider store={store}>
    <ControlPanel />
  </Provider>,
  document.getElementById('root')
);

顶层的Provider组件,通过Provider.childContextTypes来宣称自己支持context,并且提供getChildContext函数来返回代表Context的对象,Context对象中存储的值实际上是index.js导入的store,作为props传递给了Provider。这样就实现了只在index.js导入store一次。 Provider组件还会通过this.props.children把子组件渲染出来。

然后它子孙组件,只要宣称自己需要这个context,就可以通过this.context访问到这个共同的环境对象。就像这样:

SummaryContainer.contextTypes = {
  store: PropTypes.object
}

另外,构造函数也要传递context:

constructor(props, context) {
  super(props, context);
  ···
}

这里可以进一步简化为:

constructor() {
  super(...arguments);
  ···
}

React-Redux

以上过程从两个方面改进了React应用:

  • 把组件拆分为容器组件和展示组件
  • 所有组件都通过Context来访问store

实际上使用react-redux库可以代替上面的大部分工作了,不过通过前面一步步的改进过程可以了解其中的原理。

引入react-redux后,代码相比之前会更加简洁。 首先在index.js修改为从react-redux导入Provider:

import {Provider} from 'react-redux';

在子组件中,容器组件部分也交给react-redux来创建,我们只需编写展示组件部分,并定义好mapStateToProps和mapDispatchToProps。 之前在容器组件有定义getOwnState方法,将store上的状态转化为展示组件的props

getOwnState() {
  return {
    value: this.context.store.getState()[this.props.caption]
  }
}

mapStateToProps也是同样的作用:

function mapStateToProps(state, ownProps) {
  return {
    value: state[ownProps.caption]
  }
}

容器组件还定义了点击是分发action的函数,

onClickIncrementButton() {
  this.context.store.dispatch(Actions.increment(this.props.caption));
}

onClickDecrementButton() {
  this.context.store.dispatch(Actions.decrement(this.props.caption));
}

mapDispatchToProps的也是同样的作用,将dispatch传递给展示组件的props,供其主动触发:

function mapDispatchToProps(dispatch, ownProps) {
  return {
    onIncrement: () => {
      dispatch(Actions.increment(ownProps.caption));
    },
    onDecrement: () => {
      dispatch(Actions.decrement(ownProps.caption));
    }
  }
}

mapStateToProps和mapDispatchToProps会作为参数传递给react-redux的connect

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

connect函数的运行结果还是一个函数,然后把展示组件Counter作为参数调用这个函数,这个过程就类似于之前的容器组件嵌套展示组件。

参考书籍

《深入浅出React和Redux》 程墨

06-14 19:59