《深入浅出React和Redux》(2) - Redux
### 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(
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》 程墨