第三章 从Flux到Redux
-
MVC
-
Flux
使用flux:
整体目录
思路:story继承并扩展了EventEmitter对象,拥有了广播和添加/移除监听的功能。
view层通过story获取数据,并添加对该数据的监听,一旦接收到对应数据变化的广播,立即更新view层数据,view层不能直接改变数据,唯一的方式是通过派发action,action改变store数据并进行广播。
安装:
npm install --save flux
Dispatcher
AppDispatcher.js
import {Dispatcher} from 'flux';
export default new Dispatcher();
- action
ActionTypes.js
export const INCREMENT = 'increment';
export const DECREMENT = 'decrement';
Actions.js
import * as ActionTypes from './ActionTypes.js';
import AppDispatcher from './AppDispatcher.js';
export const increment = (counterCaption) => {
AppDispatcher.dispatch({
type: ActionTypes.INCREMENT,
counterCaption: counterCaption
});
};
export const decrement = (counterCaption) => {
AppDispatcher.dispatch({
type: ActionTypes.DECREMENT,
counterCaption: counterCaption
});
};
- Store
CounterStore.js
import AppDispatcher from '../AppDispatcher.js';
import * as ActionTypes from '../ActionTypes.js';
import {EventEmitter} from 'events';
const CHANGE_EVENT = 'changed';
const counterValues = {
'First': 0,
'Second': 10,
'Third': 30
};
const CounterStore = Object.assign({}, EventEmitter.prototype, {
getCounterValues: function() {
return counterValues;
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
CounterStore.dispatchToken = AppDispatcher.register((action) => {
if (action.type === ActionTypes.INCREMENT) {
counterValues[action.counterCaption] ++;
CounterStore.emitChange();
} else if (action.type === ActionTypes.DECREMENT) {
counterValues[action.counterCaption] --;
CounterStore.emitChange();
}
});
export default CounterStore;
SummaryStore.js
import AppDispatcher from '../AppDispatcher.js';
import * as ActionTypes from '../ActionTypes.js';
import CounterStore from './CounterStore.js';
import {EventEmitter} from 'events';
const CHANGE_EVENT = 'changed';
function computeSummary(counterValues) {
let summary = 0;
for (const key in counterValues) {
if (counterValues.hasOwnProperty(key)) {
summary += counterValues[key];
}
}
return summary;
}
const SummaryStore = Object.assign({}, EventEmitter.prototype, {
getSummary: function() {
return computeSummary(CounterStore.getCounterValues());
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
SummaryStore.dispatchToken = AppDispatcher.register((action) => {
if ((action.type === ActionTypes.INCREMENT) ||
(action.type === ActionTypes.DECREMENT)) {
AppDispatcher.waitFor([CounterStore.dispatchToken]);
SummaryStore.emitChange();
}
});
export default SummaryStore;
- View
ControlPanel.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 (
);
}
}
export default ControlPanel;
Counter.js
import React, { Component, PropTypes } from 'react';
import * as Actions from '../Actions.js';
import CounterStore from '../stores/CounterStore.js';
const buttonStyle = {
margin: '10px'
};
class Counter extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
this.onClickDecrementButton = this.onClickDecrementButton.bind(this);
this.state = {
count: CounterStore.getCounterValues()[props.caption]
}
}
shouldComponentUpdate(nextProps, nextState) {
return (nextProps.caption !== this.props.caption) ||
(nextState.count !== this.state.count);
}
componentDidMount() {
CounterStore.addChangeListener(this.onChange);
}
componentWillUnmount() {
CounterStore.removeChangeListener(this.onChange);
}
onChange() {
const newCount = CounterStore.getCounterValues()[this.props.caption];
this.setState({count: newCount});
}
onClickIncrementButton() {
Actions.increment(this.props.caption);
}
onClickDecrementButton() {
Actions.decrement(this.props.caption);
}
render() {
const {caption} = this.props;
return (
{caption} count: {this.state.count}
);
}
}
Counter.propTypes = {
caption: PropTypes.string.isRequired
};
export default Counter;
Summary.js
import React, { Component } from 'react';
import SummaryStore from '../stores/SummaryStore.js';
class Summary extends Component {
constructor(props) {
super(props);
this.onUpdate = this.onUpdate.bind(this);
this.state = {
sum: SummaryStore.getSummary()
}
}
componentDidMount() {
SummaryStore.addChangeListener(this.onUpdate);
}
componentWillUnmount() {
SummaryStore.removeChangeListener(this.onUpdate);
}
onUpdate() {
this.setState({
sum: SummaryStore.getSummary()
})
}
render() {
return (
Total Count: {this.state.sum}
);
}
}
export default Summary;
-
Redux
Redux实例:
安装:
npm install --save redux
action
ActionTypes.js
export const INCREMENT = 'increment';
export const DECREMENT = 'decrement';
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
};
};
- 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;
- 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
}
}
- view
ControlPanel.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 (
);
}
}
export default ControlPanel;
Counter.js
import React, { Component, PropTypes } from 'react';
import store from '../Store.js';
import * as Actions from '../Actions.js';
const buttonStyle = {
margin: '10px'
};
class Counter extends Component {
constructor(props) {
super(props);
this.onIncrement = this.onIncrement.bind(this);
this.onDecrement = this.onDecrement.bind(this);
this.onChange = this.onChange.bind(this);
this.getOwnState = this.getOwnState.bind(this);
this.state = this.getOwnState();
}
getOwnState() {
return {
value: store.getState()[this.props.caption]
};
}
onIncrement() {
store.dispatch(Actions.increment(this.props.caption));
}
onDecrement() {
store.dispatch(Actions.decrement(this.props.caption));
}
onChange() {
this.setState(this.getOwnState());
}
shouldComponentUpdate(nextProps, nextState) {
return (nextProps.caption !== this.props.caption) ||
(nextState.value !== this.state.value);
}
componentDidMount() {
store.subscribe(this.onChange);
}
componentWillUnmount() {
store.unsubscribe(this.onChange);
}
render() {
const value = this.state.value;
const {caption} = this.props;
return (
{caption} count: {value}
);
}
}
Counter.propTypes = {
caption: PropTypes.string.isRequired
};
export default Counter;
Summary.js
import React, { Component } from 'react';
import store from '../Store.js';
class Summary extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.state = this.getOwnState();
}
onChange() {
this.setState(this.getOwnState());
}
getOwnState() {
const state = store.getState();
let sum = 0;
for (const key in state) {
if (state.hasOwnProperty(key)) {
sum += state[key];
}
}
return { sum: sum };
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.sum !== this.state.sum;
}
componentDidMount() {
store.subscribe(this.onChange);
}
componentWillUnmount() {
store.unsubscribe(this.onChange);
}
render() {
const sum = this.state.sum;
return (
Total Count: {sum}
);
}
}
export default Summary;
-
容器组件和傻瓜组件
负责和Redux Store打交道的组件,处于外层,所以被称为容器组件(Container Component);只负责渲染界面的组件,处于内层,叫做展示组件(Presentational Component)。
改进Redux例子的view层:
Counter.js
import React, { Component, PropTypes } from 'react';
import store from '../Store.js';
import * as Actions from '../Actions.js';
const buttonStyle = {
margin: '10px'
};
class Counter extends Component {
render() {
const {caption, onIncrement, onDecrement, value} = this.props;
return (
{caption} count: {value}
);
}
}
Counter.propTypes = {
caption: PropTypes.string.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
value: PropTypes.number.isRequired
};
class CounterContainer extends Component {
constructor(props) {
super(props);
this.onIncrement = this.onIncrement.bind(this);
this.onDecrement = this.onDecrement.bind(this);
this.onChange = this.onChange.bind(this);
this.getOwnState = this.getOwnState.bind(this);
this.state = this.getOwnState();
}
getOwnState() {
return {
value: store.getState()[this.props.caption]
};
}
onIncrement() {
store.dispatch(Actions.increment(this.props.caption));
}
onDecrement() {
store.dispatch(Actions.decrement(this.props.caption));
}
onChange() {
this.setState(this.getOwnState());
}
shouldComponentUpdate(nextProps, nextState) {
return (nextProps.caption !== this.props.caption) ||
(nextState.value !== this.state.value);
}
componentDidMount() {
store.subscribe(this.onChange);
}
componentWillUnmount() {
store.unsubscribe(this.onChange);
}
render() {
return
}
}
CounterContainer.propTypes = {
caption: PropTypes.string.isRequired
};
export default CounterContainer;
Summary.js
import React, { Component, PropTypes } from 'react';
import store from '../Store.js';
class Summary extends Component {
render() {
return (
Total Count: {this.props.sum}
);
}
}
Summary.propTypes = {
sum: PropTypes.number.isRequired
};
class SummaryContainer extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.state = this.getOwnState();
}
onChange() {
this.setState(this.getOwnState());
}
getOwnState() {
const state = store.getState();
let sum = 0;
for (const key in state) {
if (state.hasOwnProperty(key)) {
sum += state[key];
}
}
return { sum: sum };
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.sum !== this.state.sum;
}
componentDidMount() {
store.subscribe(this.onChange);
}
componentWillUnmount() {
store.unsubscribe(this.onChange);
}
render() {
return (
);
}
}
export default SummaryContainer;
注意: 上图中的函数,还有一种惯常的写法,就是把结构赋值直接放到参数部分。
-
组件Context
进一步优化redux
创建一个名为Provider的组件:
import {PropTypes, Component} from 'react';
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
import React from 'react';
import ReactDOM from 'react-dom';
import ControlPanel from './views/ControlPanel';
import './index.css';
import store from './Store.js';
import Provider from './Provider.js';
ReactDOM.render(
,
document.getElementById('root')
);
子组件中使用context
Counter.js
import React, { Component, PropTypes } from 'react';
import * as Actions from '../Actions.js';
const buttonStyle = {
margin: '10px'
};
class Counter extends Component {
render() {
const {caption, onIncrement, onDecrement, value} = this.props;
return (
{caption} count: {value}
);
}
}
Counter.propTypes = {
caption: PropTypes.string.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
value: PropTypes.number.isRequired
};
class CounterContainer extends Component {
// 此处构造函数需要声明context
constructor(props, context) {
super(props, context);
this.onIncrement = this.onIncrement.bind(this);
this.onDecrement = this.onDecrement.bind(this);
this.onChange = this.onChange.bind(this);
this.getOwnState = this.getOwnState.bind(this);
this.state = this.getOwnState();
}
getOwnState() {
return {
value: this.context.store.getState()[this.props.caption]
};
}
onIncrement() {
this.context.store.dispatch(Actions.increment(this.props.caption));
}
onDecrement() {
this.context.store.dispatch(Actions.decrement(this.props.caption));
}
onChange() {
this.setState(this.getOwnState());
}
shouldComponentUpdate(nextProps, nextState) {
return (nextProps.caption !== this.props.caption) ||
(nextState.value !== this.state.value);
}
componentDidMount() {
this.context.store.subscribe(this.onChange);
}
componentWillUnmount() {
this.context.store.unsubscribe(this.onChange);
}
render() {
return
}
}
CounterContainer.propTypes = {
caption: PropTypes.string.isRequired
};
// 这里要和Provide里的值保持一致
CounterContainer.contextTypes = {
store: PropTypes.object
}
export default CounterContainer;
-
React-Redux
利用该库重写redux例子
首相入口文件,我们不需要自己去实现Provider,直接从库里引用:
index.js
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(
,
document.getElementById('root')
);
view层:
Counter.js
import React, { PropTypes } from 'react';
import * as Actions from '../Actions.js';
import {connect} from 'react-redux';
const buttonStyle = {
margin: '10px'
};
function Counter({caption, onIncrement, onDecrement, value}) {
return (
{caption} count: {value}
);
}
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);
Summary.js
import React, { PropTypes } from 'react';
import {connect} from 'react-redux';
function Summary({value}) {
return (
Total Count: {value}
);
}
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);