第3章 从Flux到Redux
第3章 从Flux到Redux
3.1 Flux
单向数据流,React是用来替换Jquery的,Flux是以替换Backbone.js、Ember.js等MVC框架为主的。
actionTypes.js定义action类型;
actions.js定义action构造函数,决定了这个功能模块可以接受的动作;
reducer.js定义这个功能模块如何响应action.js中定义的动作;
views目录,包含这个功能模块中所有的React组件,包括傻瓜组件和容器组件;
index.js这个文件把所有的角色导入,然后统一导出。
一个Flux应用包含四个部分:
Dispatcher,处理动作分发,维持Stror之间的关系;
Store,负责存储数据和处理数据相关逻辑;
Action,驱动dispatcher和JavaScript对象;
View,视图部分,负责显示用户界面。
如果非要把Flux 和MVC 做一个结构对比,那么, Flux 的Dispatcher 相当于MVC 的Controller, Flux 的Store 相当于MVC 的Model, Flux 的View 当然就对应MVC 的View了,至于多出来的这个Action ,可以理解为对应给MVC 框架的用户请求。
MVC最大的缺点就是无法禁绝View和Model之间的直接对话。
Flux应用实例:
1、Dispatcher
首先创造一个Dispatcher,Dispatcher的作用就是派发action,几乎所有应用都只需拥有一个Dispathcer,在src下创造一个唯一的Dispatcher对象
import {Dispatcher} from 'flux' export default new Dispacther()
2、Action
代表一个动作,一个纯粹的数据对象。
action对象必须有一个名为type的字段,代表这个action对象的类型,为了记录日志和debug方便,这个type应该是字符串类型。
定义action通常需要两个文件,一个定义action类型,一个定义actio的构造函数(也称为action creator)。原因是store会根据action不同类型做不同操作。
在src/Actiontypes.js中,定义action的类型
//两个action类型 INCREMENT和DECREMENT,一个是点击+按扭,一个是点击-按扭
export const INCREMENT = 'increment' export const DECREMENT = 'decrement'
在src/Action.js中,定义action的构造函数,这里边定义的并不是action对象本身,而是能够产生并派发action对象的函数。
//引入ActionTypes和AppDispatcher,直接使用Dispatcher
import * as ActionTypes from './ActionTypes' import AppDispatcher from './AppDispatcher'
//Action.js导出了两个action的构造函数increment和decrement,当这两个构造函数被调用的时候,创造了对象的action对象,并立即通过AppDispatcher.dispatch函数派发出去。 export const increment = (counterCaption) => { AppDispatcher.dispatch({ type: ActionTypes.INCREMENT, counterCaption: counterCaption }) } export const decrement = (counterCaption) => { AppDispatcher.dispatch({ type: ActionTypes.DECREMENT, counterCaption: counterCaption }) }
3、Store
一个Store也是一个对象,这个对象存储应用状态,同时还要接受Dispatcher派发的动作,根据动作来决定是否要更新应用状态。
当Store的状态发生变化时,需要通知应用的其它部分做出响应,做出响应的当然是view部分,但是我们硬编码这种联系,应该用消息的方式建立Store和View的联系。这就是为什么让CounterStore 扩展成了EventEmitter.proptype,等于让CounterStore成了一个EventEmitter对象。
一个EventEmitter的实例对象支持下列相关函数:
emit函数,可以广播一个特定事件,第一个参数是字符串类型的事件名称;
on函数,可以增加一个挂在这个EventEmitter对象特定事件上的处理函数,第一个参数是字符串类型的事件名称,第二个参数是处理函数;
removeListener函数,和on函数做的事情相反,删除挂在这个EventEmitter对象特定事件上的处理函数,参数一样。
import AppDispatcher from '../AppDispatcher' import * as ActionTypes from '../ActionTypes' import {EventEmitter} from 'events' import { Action } from 'rxjs/internal/scheduler/Action'; const CHANGE_EVENT = 'change' const counterValues = { 'First': 0, 'Second': 10, 'Third': 20 } const CounterStore = Object.assign({},EventEmitter.prototype,{ //让应用的其它模块可以读取当前的计数值 getCounterValues:function(){ return counterValues }, //对CounterStore状态更新的广播 emitChange:function(){ this.emit(CHANGE_EVENT) }, //添加监听函数 addChangeListener:function(callback){ this.on(CHANGE_EVENT, callback) }, //删除监听函数 removeChangeListener:function(callback){ this.removeListener(CHANGE_EVENT, callback) } })
把CounterStore注册到全局唯一的Dispatcher上去。Dispatcher有一个函数叫register,接受一个回调函数作为参数。返回值是一个token,这个token用于Sotre之前的同步,在CounterStore中暂时用不到。
//把register返回值保存在CounterStore对应的dispatchToken字段上 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
Flux的核心:当通register函数把一个回调函数注册到Dispatcher之后,所有派发给Dispatcher的action对象,都会传递到这个回调函数中来。
import React, {Component} from 'react' import Counter from './Counter' class ControlPanel extends Component{ render() { return ( <div> <Counter caption='First' /> <Counter caption='Second' /> <Counter caption='Third' /> </div> ) } } export default ControlPanel
import React, {Component} from 'react' import * as Actions from '../Action' import CounterStore from '../stores/CounterStore' const buttonStyle = { margin: '10px' } class Counter extends Component{ constructor(props){ super(props) // console.log('enter constructor',props.caption) this.add = this.add.bind(this) this.math = this.math.bind(this) this.onChange = this.onChange.bind(this) this.state={ count: CounterStore.getCounterValues()[props.caption] } } shouldComponentUpdate(nextProps, nextState){ return (nextProps.caption !== this.state.caption || nextProps.count !== this.state.count) } componentDidMount(){ CounterStore.addChangeListener(this.onChange) } componentWillUnmount(){ CounterStore.removeAllListeners(this.onChange) } onChange(){ const newCount = CounterStore.getCounterValues()[this.props.caption] this.setState({count: newCount}) } add(){ Actions.increment(this.props.caption) } math() { Actions.decrement(this.props.caption) } render(){ console.log('enter render', this.props.caption) return( <div> <button style={buttonStyle} onClick={this.add}>+</button> <button style={buttonStyle} onClick={this.math}>-</button> <span>count:{this.state.count}</span> {/* <span>props:{this.props}</span> */} </div> ) } } Counter.defaultProps={ initValue: 0, onUpdate: f => f //默认是一个什么都不做的函数 } export default Counter
4、View
存在于flux框架中的React组件需要实现以下几个功能:
创建时要读取Store上状态来初始化组件内部状态;
当Store上状态发生变化时,组件要立刻同步更新内部保持状态一致;
View如果要改变Store状态,必须并且只能派发action。
5、Flux的不足
1.Store之间的依赖关系
在Flux体系中,如果两个Store之间有逻辑关系,就必须用上Dispatcher的waitFor函数。
2.难以进行服务器渲染
3.Store混杂了逻辑和状态
Store封装了数据和处理数据的逻辑,当我们需要替换一个Store的逻辑时,只能把整个Store整体替换掉,无法保持Store中的存储状态。
3.2 Redux
Redux的三个基本原则:
唯一数据源(Single Source of Truth);
保持状态只读(State is read-only);
数据改变只能通过纯函数完成(Changes are made with pure functions)。
1、唯一数据源
唯一数据源指的是应用的状态数据应该只存储在唯一的一个Store上。
Flux是状态数据分散在多个Store中,容易造成数据冗余。
Redux就是整个应用只有一个Store,所有组件的数据源就是这个Store上的状态。
这个唯一Store上的状态,是一个树的形象。
2、保持状态只读
不能去直接修改状态,要修改Store的状态,必须要通过派发一个action对象完成,这一点和flux没区别。
3、数据改变只能通过纯函数完成
纯函数就是Reducer,Redux就是Reducer+Flux。
3.2.2 Redux实例
ActionTypes.js
export const INCREMENT = 'increment' export const DECREMENT = 'decrement'
Action.js
import * as ActionTypes from './ActionTypes' export const increment = (counterCaption) => { //Redux返回一个action 对象 return { type: ActionTypes.INCREMENT, counterCaption: counterCaption } } export const decrement = (counterCaption) => { return { type: ActionTypes.DECREMENT, counterCaption: counterCaption } }
Store.js
这个文件输出全局唯一的Store
import {createStore} from 'redux' import reducer from './Reducer' const initValues = { 'First' : 0, 'Secont' : 10, 'Third' : 20 } //第一个参数代表更新状态,第二个参数代表初始状态,第三个参数Store Enhancer,暂时用不上 const store = createStore(reducer, initValues) export default store
Reducer.js
和Flux应用中注册的回调函数一样,与Flux不同的是多了一个state,Flux中没有,是因为state是由Store管理而不是由Flux。
Redux把存储state的工作抽取出来交给Redux框架本身,让reducer只关心如何更新state,而不管state怎么存。
import * as ActionTypes from './ActionTypes' export default (state, action) => { const {counterCaption} = action /** * return {...state,[counterCaption] : state[counterCaption] + 1}等同于 * const newState = Object.assign({},state) * newState[counterCaption] ++ * return newState */ switch (action.type) { case ActionTypes.INCREMENT: //...操作符,表示把state所有字段扩展开 return {...state,[counterCaption] : state[counterCaption] + 1} case ActionTypes.DECREMENT: return { ...state, [counterCaption]: state[counterCaption] - 1 } default: return state; } }
ControlPanel.js
import React, {Component} from 'react' import Counter from './Counter' import SumCounter from './SumCounter' class ControlPanel extends Component{ render() { return ( <div> <Counter caption='First' /> <Counter caption='Second' /> <Counter caption='Third' /> <SumCounter /> </div> ) } } export default ControlPanel
Counter.js
import React, {Component} from 'react' import * as Actions from '../Action' import store from '../Store' const buttonStyle = { margin: '10px' } class Counter extends Component{ constructor(props){ super(props) // console.log('enter constructor',props.caption) this.add = this.add.bind(this) this.math = this.math.bind(this) this.getOwnState = this.getOwnState.bind(this) this.onChange = this.onChange.bind(this) this.state = this.getOwnState() } getOwnState(){ // console.log(store.getState(),'state') return { value: store.getState()[this.props.caption] } } componentDidMount(){ //通过Store的subscribe监听其变化,只要Store的状态发生变化就会调用这个组件的onChange方法。 //增加监听的函数也可以写到构造函数中 store.subscribe(this.onChange) } componentWillUnmount(){ //把监听注销 store.unsubscribe(this.onChange) } onChange(){ this.setState(this.getOwnState()) } add(){ //派发action //action构造函数只负责创建对象,要派发action就需要调用store.dispatch函数 store.dispatch(Actions.increment(this.props.caption)) // Actions.increment(this.props.caption) } math() { store.dispatch(Actions.decrement(this.props.caption)) // Actions.decrement(this.props.caption) } render(){ const value = this.state.value const {caption} = this.props return( <div> <button style={buttonStyle} onClick={this.add}>+</button> <button style={buttonStyle} onClick={this.math}>-</button> <span>{caption} count:{value}</span> </div> ) } } export default Counter
SumCounter.js
import React, {Component} from 'react' import store from '../Store' class SumCounter extends Component{ constructor(props){ super(props) this.onChange = this.onChange.bind(this) this.getOwnState = this.getOwnState.bind(this) store.subscribe(this.onChange) this.state = this.getOwnState() } onChange(){ this.setState(this.getOwnState()) } componentWillUnmount(){ store.unsubscribe(this.onChange) } getOwnState(){ const state = store.getState() let sum = 0 for(const key in state){ if(state.hasOwnProperty(key)){ sum += state[key] } } return {sum} } render(){ return( <div>Total: {this.state.sum}</div> ) } } export default SumCounter
3.2.3 容器组件和傻瓜组件
在Redux框架下,一个React组件需要完成两个功能:
1.和Redux Store打交道,读取Store状态,用于初始化组件的状态,同时还要监听Store的状态改变;当Store发生改变时,需要更新组件状态,驱动组件重新渲染;当需要更新
Store状态时,就要派发action对象;
2.根据当前props和state,渲染出用户界面。
为了让一个组件只专注做一件事,可以把这个组件拆分成多个组件,让每个组件只专注做一件事。
上面也说了在Redux框架下,一个React组件南非要完成两个功能,可以考虑拆分成两个组件,把两个组件嵌套起来,完成原本一个组件完成的所有任务。
两个组件是父子关系。承担第一个任务的组件,也就是负责和Redux Store打交道的组件,处于外层,所以被称为容器组件(Container Component),又叫聪明组件;对于承担第二个任务的组件,也就是只专心负责渲染页面的组件,处于内层,叫做展示组件,又叫傻瓜组件。
傻瓜组件就是一个纯函数,根据props产生的结果。
Counter.js拆分
import React, {Component} from 'react' import * as Actions from '../Action' import store from '../Store' const buttonStyle = { margin: '10px' } /* * 傻瓜组件 * Counter组件完全没有state,只有一个render方法,所有的数据都来自props,这种组件叫“无状态”组件 */ // class Counter extends Component{ // render(){ // const {caption, add, math, value} = this.props // return( // <div> // <button style={buttonStyle} onClick={add}>+</button> // <button style={buttonStyle} onClick={math}>-</button> // <span>{caption} count:{value}</span> // </div> // ) // } // } //缩减版傻瓜组件,无状态组件,因为没有状态,不需要对象表示,所以连类都不需要了,对于只有一个render方法的组件,缩略为一个函数足矣。 function Counter({ caption, add, math, value }){ //解构赋值或者props参数 // function Counter(props) { // const { caption, add, math, value } = props return ( <div> <button style={buttonStyle} onClick={add}>+</button> <button style={buttonStyle} onClick={math}>-</button> <span>{caption} count:{value}</span> </div> ) } //容器组件 /** * CounterContainer承担了所有的和Store关联的工作,它的render函数所做的就是渲染傻瓜组件Counter而已,只负责传递必要的prop */ class CounterContainer extends Component{ constructor(props){ super(props) // console.log('enter constructor',props.caption) this.add = this.add.bind(this) this.math = this.math.bind(this) this.getOwnState = this.getOwnState.bind(this) this.onChange = this.onChange.bind(this) this.state = this.getOwnState() } getOwnState(){ // console.log(store.getState(),'state') return { value: store.getState()[this.props.caption] } } componentDidMount(){ //通过Store的subscribe监听其变化,只要Store的状态发生变化就会调用这个组件的onChange方法。 //增加监听的函数也可以写到构造函数中 store.subscribe(this.onChange) } componentWillUnmount(){ //把监听注销 store.unsubscribe(this.onChange) } onChange(){ this.setState(this.getOwnState()) } add(){ //派发action //action构造函数只负责创建对象,要派发action就需要调用store.dispatch函数 store.dispatch(Actions.increment(this.props.caption)) // Actions.increment(this.props.caption) } math() { store.dispatch(Actions.decrement(this.props.caption)) // Actions.decrement(this.props.caption) } render(){ const value = this.state.value const {caption} = this.props return( <Counter caption={caption} add={this.add} math={this.math} value={value} /> ) } } //export导出的是容器组件,对于这个视图模块来说,根本不会感受到傻瓜组件的存在,从外部看到的就只是容器组件。 export default CounterContainer
3.2.4 组件Context
不能每一个组件用到Store都去引入一次,需要定义一个全局的,只有一个地方需要导入Store,这个位置应该在调用最顶层的React的位置。
Context就是“上下文环境”,让一个树状组件上的所有组件都能访问一个共同的对象。
首先上级要宣称自己支持context,并且提供一个函数来返回代表Context的对象。
然后,这个上级组件下的所有子孙组件,只需要宣称自己需要这个context,就可以通过this.context访问到这个共同的环境对象。
定义一个Provider.js组件
import {Component} from 'react' import PropTypes from 'prop-types' /** * Provider也是一个React组件,它的render函数就是简单的把子组件渲染出来,在渲染上,Provider不做作何附加的事情。 */ class Provider extends Component{ //这个函数返回的就是代表Context的对象 //要求Provider的使用者通过prop传递进来store getChildContext(){ return { store: this.props.store } } render(){ /** * 每个React组件的props中都可以有一个特殊属性children,代表的是子组件 * this.props.children就是两个Provider标签之间的<ControlPanel /> * <Provider> * <ControlPanel /> * </Provider> */ //把渲染工作交给子组件 return this.props.children } } //为了让Provider能够被React认可为一个Context的提供者,还需要指定Provider的childContextTypes属性 //类的childContextTypes,必须和getChildContext对应,只有两者都齐备,Provider的子组件才有可能访问到context. Provider.childContextTypes = { store: PropTypes.object } export default Provider
入口文件index.js
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import Provider from './redux_width_context/Provider' import store from './redux_width_context/Store' import ControlPanel from './redux_width_context/views/ControlPanel' ReactDOM.render( <Provider store={store}> <ControlPanel /> </Provider> , document.getElementById('root'))
ControlPanel.js
import React, {Component} from 'react' import Counter from './Counter' import SumCounter from './SumCounter' class ControlPanel extends Component{ render(){ return( <div> <Counter caption='First' /> <Counter caption='Second' /> <Counter caption='Third' /> <SumCounter /> </div> ) } } export default ControlPanel
Counter.js
import React, {Component} from 'react' import * as Actions from '../Action' // import store from '../Store' import PropTypes from 'prop-types' const buttonStyle={ margin: '10px' } function Counter(props){ const {cpation, add, math, value} = props return ( <div> <button style={buttonStyle} onClick={add}>+</button> <button style={buttonStyle} onClick={math}>-</button> <span>{cpation} Count: {value}</span> </div> ) } class CounterContainer extends Component{ //因为定义了自己的构造函数,所以要用上第二个参数context constructor(props, context){ console.log(context, context) //super的时候要带上context,这样才能上React组件初始化实例中的context,不然组件的其它部分就无法使用this.context //也可以用...arguments的方法 super(props,context) this.getOwnState = this.getOwnState.bind(this) this.add = this.add.bind(this) this.math = this.math.bind(this) this.onChange = this.onChange.bind(this) this.context.store.subscribe(this.onChange) this.state=this.getOwnState() } getOwnState(){ return { // value: store.getState()[this.props.caption] //所有的store访问者是通过this.context.store完成,this.context就是Provider提供的context对象 value: this.context.store.getState()[this.props.caption] } } componentWillUnmount(){ this.context.store.unsubscribe(this.onChange) } onChange(){ this.setState(this.getOwnState()) } add(){ this.context.store.dispatch(Actions.increment(this.props.caption)) } math(){ this.context.store.dispatch(Actions.decrement(this.props.caption)) } render(){ const value = this.state.value return( <Counter caption={this.props} add={this.add} math={this.math} value={value} /> ) } } /** * 给CounterContainer类的contextTypes赋值和Provider.childContextTypes一样的值,两者必须一致,不然无法访问到context */ CounterContainer.contextTypes = { store: PropTypes.object } export default CounterContainer
3.2.5 React-Redux
React应用改进的两个方法,
第一是把一个组件拆分为容器组件和傻瓜组件,第二是使用React的Context来提供一个所有组件都可以直接访问的Context。
React-Redux就是把这两种方法的套路部分抽取出来复用,这样每个组件的开发只需关注不同的部分就可以了。
React-Redux和Redux的不同就是react-redux不再使用自己实现的Provider,而是从react-redux库导入Provider,
react-redux的两个最主要功能:
connect:连接容器组件和傻瓜组件
Provider:提供包含store的context。
1、connect函数具体做的工作:
把Store上的状态转化为内层傻瓜组件的prop;
把内层傻瓜组件中的用户动作转化为派送给Store的动作。
一个是内层傻瓜对象的输入,一个是内层傻瓜对象的输出。
Counter.js
import React, {Component} from 'react' import * as Actions from '../Action' // import store from '../Store' import PropTypes from 'prop-types' import { connect } from 'react-redux'; const buttonStyle={ margin: '10px' } function Counter(props){ const {cpation, onIncrement, onDecrement, value} = props return ( <div> <button style={buttonStyle} onClick={onIncrement}>+</button> <button style={buttonStyle} onClick={onDecrement}>-</button> <span>{cpation} Count: {value}</span> </div> ) } /** * 把Store上的状态转化为内层组件的Props * @param {*} state * @param {*} ownProps */ function mapStateToProps(state, ownProps){ return { value: state[ownProps.caption] } } /** * 把内层傻瓜组件中用户动作转化为派送给Store的动作,也就是把内层傻瓜组件暴露出来的函数类型的prop关联上dispatch函数的调用, * 每个prop代表的回调函数的主要区别就是dispatch函数的参数不同。 * @param {*} dispatch * @param {*} ownProps */ function mapDispatchToProps(dispatch, ownProps){ return { onIncrement: () => { dispatch(Actions.increment(ownProps.caption)) }, onDecrement: () => { dispatch(Actions.decrement(ownProps.caption)) } } } /** * 给CounterContainer类的contextTypes赋值和Provider.childContextTypes一样的值,两者必须一致,不然无法访问到context */ CounterContainer.contextTypes = { store: PropTypes.object } /** * connect是react-redux提供的一个方法 * 接收两个参数,执行结果依然是一个函数, * 这里有两个函数执行: * 第一次是connect函数的执行; * 第二次把Connect函数执行的结果再执行,这一次的参数是Counter傻瓜组件,最后产生的就是容器组件。 */ export default connect(mapStateToProps, mapDispatchToProps)(Counter)
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; // import Provider from './redux_width_context/Provider' import {Provider} from 'react-redux' import store from './redux_width_context/Store' import ControlPanel from './react_redux/views/ControlPanel' ReactDOM.render( <Provider store={store}> <ControlPanel /> </Provider> , document.getElementById('root'))
2、Provider
react-redux要求store不光是一个object,而且必须是包含三个函数的object,这三个函数分别是:
subscribe、dispatch、getState
react-redux定义了Provider的componentWillReceiveProps函数,在React组件的生命周期中,componentWillReceiveProps函数在每次重新渲染时都会调用到,react-redux在componentWillReceiveProps函数中会检查这一次渲染时代表store的prop和上一次的是否一样。如果不一样,就会给出警告,必免多次渲染了不同的Redux Store。
每个Redux应用都只能有一个Redux Store,在整个Redux的生命周期中都应保持Store的唯一性。