本文中的代码来自《深入浅出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。