第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的唯一性。

 

posted on 2018-12-23 12:09  huyanluanyu1989  阅读(217)  评论(0编辑  收藏  举报