redux架构基础

本文书接 从flux到redux , 是《深入浅出react和redux》为主的比较阅读笔记。

redux架构基础

“如果你愿意限制做事方式的灵活度,你几乎总会发现可以做得更好。”——John Carmark

redux的官方定义是:

Redux is a predictable static container for JavaScript apps.

按照作者Dan Abramov的说法,Redux名字的含义是Reducer+Flux。Reducer不是一个Redux特定的术语,而是一个计算机科学中的通用概念,很多语言和框架都有对Reducer函数的支持。就以JavaScript为例,数组类型就有reduce函数,接受的参数就是一个reducer,reduce做的事情就是把数组所有元素依次做“规约”,对每个元素都调用一次参数reducer,通过reducer函数完成规约所有元素的功能

笔者的理解是:redux既不操作dom,也不践行MVC,而是专注于状态管理。它就是一个体积很小且优雅的,规定了使用模式的库。

和flux一样,redux和react也没有必然的联系。redux是flux设计哲学的又一种实现。

redux的哲学思想

single source of trues

"真相,单一的真相"

无论是计数器,还是一个什么牛逼哄哄的聊天软件,整个应用的的状态来源于一个唯一store,(sotore.getState)。

Redux并没有阻止一个应用拥有多个Store,只是,在Redux的框架下,让一个应用拥有多个Store不会带来任何好处,最后还不如使用一个Store更容易组织代码。这个唯一Store上的状态,是一个树形的对象,每个组件往往只是用树形对象上一部分的数据,而如何设计Store上状态的结构,就是Redux应用的核心问题。

state is readonly

"状态,只读的状态"

这条哲学不是让你如何去塑造一个"不可写"的state,而是告诉你,必须通过派发(dispatch)一个action的方法改变状态:

let aaa=store.getState();aaa.bbb='ccc';

以上是错误的示范。

那么派发action怎么就能改变state呢?

changes are made with pure function called reducer

"改变,用reducer"

也就是说,action派发之后,响应的事件将被reducer所响应。reducer处理了逻辑之后,store.getState拿到的状态也随之更新。

现在看来,reduce和action都需要由开发者编写。其中reduce接受两个参数,返回一个全新的状态对象:

const reducer=(preState,action)=>newState;

在《从flux到redux》一文中,我们写了一个注册方法:

// 注册的回调函数包含了业务方法  CounterStore.dispatchToken = Dispatcher.register((action) =>  {      if (action.type === ActionTypes.INCREMENT) {          counterValues[action.countrCaption] ++;          CounterStore.emitChange();      }       else if (action.type === ActionTypes.DECREMENT) {           counterValues[action.counterCaption] --;           CounterStore.emitChange();       }      }    );

在redux到表述就是reducer:

const reducer=(preState,action)=>{    const {label,type}=action;    switch type(){      case ActionTypes.INCREMENT:          return {          ...preState,          [label]:preState[label]+1;        }      case ActionTypes.DECREMENT:          return {          ...preState,          [label]:preState[label]-1;        }      default:          return preState    }  }

所以reduce不负责储存状态,只计算状态。

补白:pure function

函数式编程更在意结果而非过程。JavaScript作为"函数是一等公民"的语言,函数可以是参数,也可以是返回值:

// 面向过程计算1*(1+1)let a=1,b=1,c=1;let d=a+b;d*c; // 函数式编程 1*(1+1)const add=(a,b)=>a+b;const multify=(a,b)=>a*b;multify(1,add(1,1))

Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。

纯函数是函数式编程的概念,必须遵守以下一些约束。

•不得改写参数•不能调用系统 I/O 的API•不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果

由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。


让我们总结一下,假如你的页面出现一个bug,在本该展现数据a的地方component1,错误出现了数据2,你可以用这个思路来debug:

redux实践

现在用redux来第三次实现计数器。

安装:

npm install --save react-redux

Action

actiontype的定义和flux版本一模一样。action/index的不同在于:

const increment=(label)=>{  return {      type:ActionTypes.INCREMENT,      label      }  }  const decrement=(label)=>{  return {      type:ActionTypes.DECREMENT,      label      }  }

没有了dispatcher:Dispatcher在flux中存在的作用就是把一个action对象分发给多个注册了的Store,因为redux是是单一store,因此无需显式设置dispatcher。

store

Redux库提供的createStore函数,这个函数第一个参数代表更新状态的reducer,第二个参数是状态的初始值。

在Store下新建index.js

import {createStore} from 'redux';  import reducer from './Reducer';    // 初始状态  const counterValues = {    firstCount: 0,    secoundCount: 0,    thirdCount: 0  }    var store=createStore(reducer,initValues);    export default store;

Ruducer

  // reducer处理分发逻辑  import ActionTypes from '../Action/ActionTypes';    export default (state, action) => {      const { label } = action;      switch (action.type) {          case ActionTypes.INCREMENT:              return {                  ...state,                  [label]: state[label] + 1              };          case ActionTypes.DECREMENT:              return {                  ...state,                  [label]: state[label] - 1              }          default:              return state;      }  }

在reducer中,绝对不能去修改参数中的state。

View

现在,修改所有组件放到src/view文件夹。

在ClickCounter中,我们不再区分不同组件的状态。而是统一向store拿。初始状态可以从store.getState()[this.props.label]拿。,每个组件往往只需要使用返回状态的一部分数据。为了避免重复代码,我们把从store获得状态的逻辑放在getOwnState函数中,这样任何关联Store状态的地方都可以重用这个函数。

在componentDidMount函数中,我们通过Store的subscribe监听其变化,只要Store状态发生变化,就会调用这个组件的onChange方法;在componentWillUnmount函数中,我们把这个监听注销掉,这个清理动作和componentDidMount中的动作对应。

// view/ClickCounter.js  import React, { Component } from 'react'  import store from '../stores'  import Actions from '../Action/index'    const styles = {   //...  }    class ClickCounter extends Component {      constructor(props) {          super(props)          this.state = this.getOwnState();      }        getOwnState=()=>{          return {              count: store.getState()[this.props.label]          }      }            // 保持store和state的同步      componentDidMount() {          store.subscribe(this.onChange);      }        componentWillUnmount() {          store.unsubscribe(this.onChange);      }        onChange = () => {          this.setState(this.getOwnState());      }        // 派发      onClickIncrementButton = () => {          store.dispatch(Actions.increment(this.props.label));      }        onClickDecrementButton = () => {          store.dispatch(Actions.decrement(this.props.label));      }        render() {            return (              <div style={styles.counter}>                  <div style={styles.label}>{this.props.label}</div>                  <button onClick={this.onClickDecrementButton}>-</button>                  <div style={styles.showbox}>{this.state.count}</div>                  <button onClick={this.onClickIncrementButton}>+</button>              </div>          )      }  }    export default ClickCounter;

再来看allCount组件,一开始也是初始化一个getOwnState,通过遍历获取,再通过store.subscribe来绑定事件

import React, { Component } from 'react'  import store from '../stores';    export default class extends Component {      constructor(props) {          super(props);          this.state = this.getOwnState();      }        getOwnState() {          const state = store.getState();          let sum = 0;          Object.keys(state).forEach((key) => {              if (state.hasOwnProperty(key)) {                  sum += state[key];              }          });          return { sum: sum };      }        onChange = () => {          this.setState(this.getOwnState());      }        shouldComponentUpdate(nextProps, nextState) {          return nextState.sum !== this.state.sum;      }        componentDidMount() {          store.subscribe(this.onChange);      }        componentWillUnmount() {          store.unsubscribe(this.onChange);      }        render() {          // switch          return (              <div>Total Count: {this.state.sum}</div>          );      }  }

那么redux版的计数器功能就完成了。

容器与傻瓜

redux版计数器,组件就做两件事:

•跟store拿状态,初始化初始状态•监听store的改变,通过setState更新

这样的设计仍然是违反单一职责原则的。我们应该考虑把组件拆分为嵌套两部分:父组件负责跟store拿状态,它必须有子组件才能运行,是为"容器组件",子组件负责根据props更新界面,是为不用思考的"傻瓜组件"。如下图:

抽离这两部分有两个要点,就是容器组件应当是可复用的,而傻瓜组件不应有半点自身的思考,它是无状态的(可以是函数式组件)。

// 容器组件  class WithContainer extends Component {      constructor(props) {          super(props)          this.state = this.getOwnState();      }        getOwnState=()=>{          return {              count: store.getState()[this.props.label]          }      }        componentDidMount() {          store.subscribe(this.onChange);      }        componentWillUnmount() {          store.unsubscribe(this.onChange);      }        onChange = () => {          this.setState(this.getOwnState())      }        onClickIncrementButton = () => {          store.dispatch(Actions.increment(this.props.label));      }        onClickDecrementButton = () => {          store.dispatch(Actions.decrement(this.props.label));      }        render() {          return (              <ClickCounter                  label={this.props.label}                  count={this.state.count}                  onClickIncrementButton={this.onClickIncrementButton}                  onClickDecrementButton={this.onClickDecrementButton}              />          )      }  }    export default WithContainer

傻瓜组件就是一个纯函数:

// 傻瓜组件  function ClickCounter(props){      const {          label,          count,          onClickDecrementButton,          onClickIncrementButton      } = props;        return (          <div style={styles.counter}>              <div style={styles.label}>{label}</div>              <button onClick={onClickDecrementButton}>-</button>              <div style={styles.showbox}>{count}</div>              <button onClick={onClickIncrementButton}>+</button>          </div>      )  }

跨代传值解决方案:context

当前所有组件都是单独引入store。写起来很冗余。

一个应用中,最好只有一个地方需要直接导入Store,这个位置当然应该是在调用最顶层React组件的位置。在我们的ControlPanel例子中,就是应用的入口文件src/index.js中,其余组件应该避免直接导入Store。

不让组件直接导入Store,那就只能让组件的上层组件把Store传递下来了。首先想到的当然是用props,毕竟,React组件就是用props来传递父子组件之间的数据的。不过,这种方法有一个很大的缺陷,就是从上到下,所有的组件都要帮助传递这个props。设想在一个嵌套多层的组件结构中,只有最里层的组件才需要使用store,但是为了把store从最外层传递到最里层,就要求中间所有的组件都需要增加对这个storeprop的支持,即使根本不使用它,这无疑很麻烦。

因此就要用到react的跨代传值利器——context。

所谓Context,就是“上下文环境”,让一个树状组件上所有组件都能访问一个共同的对象,为了完成这个任务,需要上级组件和下级组件配合。

首先,上级组件要宣称自己支持context,并且提供一个函数来返回代表Context的对象。然后,这个上级组件之下的所有子孙组件,只要宣称自己需要这个context,就可以通过this.context访问到它。

我们自然想到在应用顶端宣称支持context并把store传入。为此,我们创建一个特殊的组件——Provider。

在src下新建一个Provider.js:

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.propTypes = {  store: PropTypes.object.isRequired}  Provider.childContextTypes = {  store: PropTypes.object};  export default Provider;

然后在index.js中引入Provider和store:

import React from 'react';  import ReactDOM from 'react-dom';  import './index.css';  import App from './view/App';  import * as serviceWorker from './serviceWorker';  import Provider from './view/Provider';  import store from './stores/index';      ReactDOM.render(<Provider store={store}>      <App />  </Provider>, document.getElementById('root'));    serviceWorker.unregister();

自此,Provider成为了完全意义上的顶层组件。当然,如同我们上面看到的,Provider只是把渲染工作完全交给子组件,它扮演的角色只是提供Context,包住了最顶层的ControlPanel,也就让context覆盖了整个应用中所有组件。

那么底层组件如何获取context呢?当然是修改容器组件。以ClickCount为例:

import React, { Component } from 'react';  import PropTypes from 'prop-types';  // import store from '../stores'; 不再需要引入  import Actions from '../Action/index';    /*  为了让WithContainer能够访问到context,必须给WithContainer类的contextTypes赋值和Provider.childContextTypes一样的值,两者必须一致,不然就无法访问到context,  */  WithContainer.contextTypes = {      store: PropTypes.object  }

然后就可以用this.context.store来获取store了。

class WithContainer extends Component {    /*    在调用super的时候,一定要带上context参数,这样才能让React组件初始化实例中的context,不然组件的其他部分就无法使用this.context。    */    constructor(props, context) {        super(props, context)        this.state = this.getOwnState();    }      getOwnState = () => {        return {            count: this.context.store.getState()[this.props.label]        }    }      componentDidMount() {        this.context.store.subscribe(this.onChange);    }      componentWillUnmount() {        this.context.store.unsubscribe(this.onChange);    }      onChange = () => {        this.setState(this.getOwnState())    }      onClickIncrementButton = () => {        this.context.store.dispatch(Actions.increment(this.props.label));    }      onClickDecrementButton = () => {        this.context.store.dispatch(Actions.decrement(this.props.label));    }      render() {        return (            <ClickCounter                label={this.props.label}                count={this.state.count}                onClickIncrementButton={this.onClickIncrementButton}                onClickDecrementButton={this.onClickDecrementButton}            />        )    }}

在本文中,我们学习了redux的哲学,从框架原理层面了解了如何用redux来完成React应用,并提供优化方案——第一是把一个组件拆分为容器组件和傻瓜组件,第二是使用React的Context来提供一个所有组件都可以直接访问的Context,也不难发现,这两种方法都有套路,完全可以把套路部分抽取出来复用,这样每个组件的开发只需要关注于不同的部分就可以了。

实际上本文到目前为止,从来没讲什么react-redux。实现的所有思路都是手撸。

实际上,已经有这样的一个库来完成这些工作了,这个库就是react-redux。

终极解决方案:react-redux

首先是安装react-redux:

npm i react-redux -S

redux将实现两个重要的功能:

•connect:链接容器组件和傻瓜组件。•Provider:提供包含store的context

connect

connect相当于一个容器组件的工厂。帮助我们创建了容器它的方法是cxonnect(mapStateToProps, mapDispatchToProps),connect是reactredux提供的一个方法,这个方法接收两个参数mapStateToProps和mapDispatchToProps(当无计算时,为非必传),执行结果依然是一个函数,所以才可以在后面又加一个圆括号,把connect函数执行的结果立刻执行,这一次参数是Counter这个傻瓜组件。这里有两次函数执行,第一次是connect函数的执行,第二次是把connect函数返回的函数再次执行,最后产生的就是容器组件,mapStateToProps和mapDispatchToProps都可以包含第二个参数,代表ownProps,也就是直接传递给外层容器组件的props。

// 精简后的ClickCounterimport React, { Component } from 'react';import PropTypes from 'prop-types';import Actions from '../Action/index';import { connect } from 'react-redux';    const styles = {  // ...}  // ownProps也就是直接传递给外层容器组件的props。// 把state转化为属性function mapStateToProps(state, ownProps) {    return {        count: state[ownProps.label]    }}// 定义改变逻辑,需dispatch触发:function mapDispatchToProps(dispatch, ownProps) {    return {        onClickIncrementButton: () => {            dispatch(Actions.increment(ownProps.label));        },        onClickDecrementButton: () => {            dispatch(Actions.decrement(ownProps.label));        }    }}  function ClickCounter(props) {    const {        label,        count,        onClickDecrementButton        , onClickIncrementButton    } = props;      return (        <div style={styles.counter}>            <div style={styles.label}>{label}</div>            <button onClick={onClickDecrementButton}>-</button>            <div style={styles.showbox}>{count}</div>            <button onClick={onClickIncrementButton}>+</button>        </div>    )}  export default connect(mapStateToProps, mapDispatchToProps)(ClickCounter);

在AllCounter中,写法也是大大简化

import React from 'react'import {connect} from 'react-redux';  function AllCount({ sum }) {    return <div>Total Count: {sum}</div>}    function mapStateToProps(state, ownProps) {    let sum = 0;    Object.keys(state).forEach((key) => {        if (state.hasOwnProperty(key)) {            sum += state[key];        }    });    return { sum: sum };}  export default connect(mapStateToProps)(AllCount);

Provider

Provider的用法和之前定义的几乎一致,而且不必再定义默认数据类型了:

  import React from 'react';  import ReactDOM from 'react-dom';  import './index.css';  import App from './view/App';  import * as serviceWorker from './serviceWorker';  // import Provider from './view/Provider'; 不再用自己造的轮子  import {Provider} from 'react-redux';  import store from './stores/index';      ReactDOM.render(<Provider store={store}>      <App />  </Provider>, document.getElementById('root'));    serviceWorker.unregister();

Redux是Flux框架的一个巨大改进,Redux强调单一数据源、保持状态只读和数据改变只能通过纯函数完成的基本原则,和React的UI=render(state)思想完全契合。我们在这一章中用不同方法,循序渐进的改进了计数器,为的就是更清晰地理解每个改进背后的动因,最后,我们终于通过react-redux完成了React和Redux的融合。