理解react-redux的工作方式:从一个实例谈起

本文中的代码来自《深入浅出React和Redux》这本书,特此说明,在此也对作者的工作表示感谢。

代码用react、redux结合的方式实现了一个计数器,我们一步步来看:

1、入口

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import ControlPanel from './views/ControlPanel';
import store from './Store.js';
import './index.css';

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

提供全局的provider,使用provider的动机是让处于同一个树形结构中的组件都可以访问到共有的数据。这里的provider是由react-redux这个库直接提供的,我们需要自己关联一个store,下面就来看看store.js是怎么写的:

2、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存放的是组件需要用到的数据,createStore表示创建store,第一个参数是reducer,reducer的作用是根据状态(state)和动作(action)来创建一个新状态,这个新状态会被重新写到store里。

3、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
  }
}

没什么特别的,就是根据动作来建立新的状态,第一句那个奇怪的const {counterCaption}=action;是ES6里的新语法,表示把action的counterCaption传给counterCaption,等价于const counterCaption=action.counterCaption。还要注意ES6中的...语法,这个...表示展开,例如...[1,2,3]就相当于1,2,3。return语句在逻辑上等价于:

const newState=Object.assign({},state);

newState[counterCaption]++;

return newState;

写成...那种形式只是为了简单。

那么reducer中的动作都是什么?和动作相关的有两部分东西,请看下面的4和5。

4、ActionTypes.js

定义了动作的类型:

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

就是把用户对组件做的事情定义成一个字符串。

5、Actions.js

不同动作类型对应了不同的动作:

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
  };
};

这个函数返回的其实是对象,用来在reducer里调用的。这样就可以反过去理解reducer里的代码了,其实真正干事情的就是reducer嘛。

6、view相关

最后都是视图(view)层面的事了,根据需要作者写了三个js文件:

(1)Counter.js

这是单个的计数器:

import React from 'react';
import * as Actions from '../Actions.js';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';

const buttonStyle = {
  margin: '10px'
};

function Counter({caption, onIncrement, onDecrement, value}) {
  return (
    <div>
      <button style={buttonStyle} onClick={onIncrement}>+</button>
      <button style={buttonStyle} onClick={onDecrement}>-</button>
      <span>{caption} count: {value}</span>
    </div>
  );
}

Counter.propTypes = {
  caption: PropTypes.string.isRequired,
  onIncrement: PropTypes.func.isRequired,
  onDecrement: PropTypes.func.isRequired,
  value: PropTypes.number.isRequired
};

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

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

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

这是一个无状态组件(这里也叫UI组件),写成了function的形式,它要做的事情仅仅是根据props来渲染,而props来自于外层容器组件的状态(state),但是容器组件在哪里?

这就是react-redux的重要特点:无状态组件由程序员自己定义,容器则用api生成,代码中的connect函数就起到了创建容器组件的作用,具体来说,它会连接无状态组件和Store,返回一个容器组件。但是没有业务逻辑的组件是没意义的,业务逻辑包含两方面的内容:

①外部的数据(state)怎么转换为无状态组件的props

②用户的动作(action)怎么处理

这就是我们需要mapStateToProps和mapDispatchToProps的原因。

mapStateToProps负责把外部的数据映射成无状态组件的props,具体到这个例子,函数接受容器组件的state,按照不同的caption来改变Counter的value。

mapDispatchToProps负责把用户的动作映射到store.dispatch,具体到这个例子,函数根据不同的被触发的函数(是onIncrement还是onDecrement),将不同动作派发出去,onIncrement和onDecrement在Counter.js中被关联在了onClick上,即每次点击counter就会被触发。

等等!似乎开始混乱了,我们来梳理一下点击组件时发生了什么:

点击以后,onClick事件触发,导致绑定在onClick上的onIncrement或者onDecrement触发(这取决于你点击了哪个),然后mapDispatchToProps发挥作用,返回一个对象,对象的成员名称和onClick绑定的函数一致,该成员是一个匿名函数,作用是派发相应的动作。这个动作返回的是一个对象,对象的第一个成员是动作类型,第二个成员是caption。此时store里的reducer根据现在的动作来返回一个新的state,然后这个state经过mapStateToProps的处理,最后把新的数据更新到了无状态组件上。这就是全部的过程。

(2)ControlPanal.js

将计数器组织了一下,比较简单:

import React, { Component } from 'react';
import Counter from './Counter.js';
import Summary from './Summary.js';

const style = {
  margin: '20px'
};

class ControlPanel extends Component {
  render() {
    return (
      <div style={style}>
        <Counter caption="First" />
        <Counter caption="Second" />
        <Counter caption="Third" />
        <hr/>
        <Summary />
      </div>
    );
  }
}

export default ControlPanel;

(3)Summary.js

用来求和的:

import React from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';

function Summary({value}) {
  return (
    <div>Total Count: {value}</div>
  );
}

Summary.PropTypes = {
  value: PropTypes.number.isRequired
};

function mapStateToProps(state) {
  let sum = 0;
  for (const key in state) {
    if (state.hasOwnProperty(key)) {
      sum += state[key];
    }
  }
  return {value: sum};
}

export default connect(mapStateToProps)(Summary);

因为和用户没有交互,所以没有定义mapDispatchToProps。


展开阅读全文

没有更多推荐了,返回首页