《深入浅出React和Redux》(2) – Redux

  • A+
所属分类:Web前端
摘要

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

### 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》 程墨