Flux |概念与应用

笔记,参考程墨老师的《深入浅出React和Redux》

Flux概念

image.png

  • Dispatcher,处理动作分发,维持Store之间的依赖关系
  • Store,负责存储数据和处理数据相关逻辑
  • Action,提供给Dispatcher,传递数据给Store
  • View,视图部分,负责显示用户界面

首先会产生一个事件,一般是由用户对界面执行的一个操作,Action得到这个操作后,将其交给Dispatcher,再由Dispatcher来进行分发给Store, Store收到通知后对相关数据进行维护,再发出一个更改通知,告诉视图层需要更新View了,然后重新从Store中检索数据,调用setState方法对View进行更新。

Flux应用

示例

源码分支02flux




安装

npm install --save flux

Dispatcher

src/AppDispatcher.js

//1.从flux库导入Dispatcher
//Dispatcher用来派发action
import {Dispatcher} from 'flux'
export default new Dispatcher();

Action

src/AcitonTypes.js

//1.定义actions类型
export const INCREMENT='increment'
export const DECREMENT='decrement'

src/Actions.js

//2.定义action构造函数
//	这是能够产生并派发action对象的函数(increment和decrement)
//	只要这两个函数被调用,就会创造对应的action对象 并通过AppDispatcher.dispatch派发出去
import * as AcitonTypes from './ActionTypes';
import AppDispatcher from './AppDispatcher'

export const increment = (counterCaption)=>{
    AppDispatcher.dispatch({
        type:AcitonTypes.INCREMENT,
        counterCaption:counterCaption
    })
}

export const decrement = (counterCaption)=>{
    AppDispatcher.dispatch({
        type:AcitonTypes.DECREMENT,
        counterCaption:counterCaption
    })
}

Store

store对象
- 存储应用状态
- 接受Dispatcher派发的动作
- 根据动作来决定是否要更新应用状态
[例子]有3个Counter组件,1个统计3个Counter计数值之和的组件
- 为Counter组件服务的CounterStore
- 为总数服务的SummaryStore

src/stores/CounterStore.js

import AppDispatcher from '../AppDispatcher'
import * as AcitonTypes from '../ActionTypes';
import {EventEmitter} from 'events';
const CHANGE_EVENT='changed'
const counterValues = {
    'First': 0,
    'Second': 10,
    'Third': 30
};
const CounterStore=Object.assign({},EventEmitter.prototype,{
    //1.用于让应用中其他模块读取当前的计数值
    getCounterVlues:function(){
        return conterValues
    },
    
    //2.定义监听
    emitChange:function(){
        this.emit(CHANGE_EVENT)
    },
    addChangeListener:function(callback){
        this.on(CHANGE_EVENT,callback)
    },
    removeChangeListener:function(callback){
        this.removeListener(CHANGE_EVENT,callback)
    }
})

EventEmitter.prototype可以让对象变成EventEmitter对象。EventEmitter的实例对象支持下列相关函数:

  • emit函数,广播特定事件,第一个参数是字符串类型的事件名称;
  • on函数,增加一个挂在这个EventEmitter对象特定事件上的处理函数,第一个参数是字符串类型的事件名称,第二个参数是处理函数;
  • removeListener函数,和on函数做的事情相反,删除挂在这个EventEmitter对象特定事件上的处理函数。
    三者完成:更新的广播、添加监听函数和删除监听函数
//3. 把CounterStore注册到全局唯一的Dispatcher上去
//4. Dispatcher有一个register函数,接受回调函数作为参数
//5. 返回值是一个token,这个token可以用于Store之间的同步
//	 返回值token在稍后的SummaryStore中会用到,现在保存在CounterStore.dispatchToken中
CounterStore.dispatchToken=AppDispatcher.register((action)=>{
    if(action.type===AcitonTypes.INCREMENT){
        counterValues[action.counterCaption]++
        //6.更新监听
        CounterStore.emitChange()
    }else if(action.type===AcitonTypes.DECREMENT){
        counterValues[action.counterCaption]--
        CounterStore.emitChange()
    }
})
export default CounterStore

当通过register函数把一个回调函数注册到Dispatcher之后,所有派发给Dispatcher的action对象,都会传递到这个回调函数中来。比如通过Dispatcher派发一个动作:

AppDispatcher.dispatch({
       type:AcitonTypes.INCREMENT,
       counterCaption:'First'
})
//1. 现在,当在CounterStore注册(register)的回调函数就会被调用
//	 register的参数就是这个派发的action对象
//2. 回调函数会根据action对象来决定改如何更新自己的状态
//   比如该动作含义:名为First的计数器要做加一动作。根据不同的type,会有不同的操作
//3. 所以注册的回调函数很自然有一个模式,就是函数体是if-else条件语句或者switch条件语句
//	 条件语句的跳转条件,都是针对参数action对象的type字段
//4. 无论是加一或者减一,最后都要调用CounterStore.emitChange函数。
//	 此时,如果有调用者通过CounterStore.addChangeListner关注了CounterStore的状态变化
//	 这个emitChange函数调用就会引发监听函数的执行

src/stores/SummaryStore.js

import CounterStore from './CounterStore'
import AppDispatcher from '../AppDispatcher'
import * as AcitonTypes from '../ActionTypes';
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并没有存储自己的状态。当getSummary被调用时,它是直接从CounterStore里获取状态计算。SummaryStore提供了getSummary让其他模块可以获得所有计数器当前值的总和。

SummaryStore.dispatchToken=AppDispatcher.register((action)=>{
    if((action.type===AcitonTypes.INCREMENT)||(action.type===AcitonTypes.DECREMENT)){
        AppDispatcher.waitFor([CounterStore.dispatchToken]);
        SummaryStore.emitChange()
    }
})
export default SummaryStore

//1. SummaryStore同样通过AppDispatcher.register函数注册回调函数,用于接受派发的action对象
//	 在回调函数中,只关注INCREMENT和DECREMENT类型的action对象。并通过emitChange通知监听者
//2. waitFor函数解决了调用顺序问题
//   这时,在CounterStore中注册回调函数时保存下来的dispatchToken终于派上了用场。
//   调用waitFor的时候,把控制权交给Dispatcher
//   让Dispatcher检查dispatchToken代表的回调函数有没有被执行,如果已经执行,那就直接接续;
//   如果还没有执行,那就调用dispatchToken代表的回调函数之后waitFor才返回

Dispatcher的register函数,只提供了注册一个回调函数的功能,但不能让调用者在register时选择只监听某些action。即当一个动作被派发的时候,Dispatcher就是简单地把所有注册的回调函数全都调用一遍

View

Flux框架下,View并不是说必须要使用React, View本身是一个独立的部分,可以用任何一种UI库来实现。
存在于Flux框架中的React组件需要实现以下几个功能:
1.	创建时要读取Store上状态来初始化组件内部状态;
2.	当Store上状态发生变化时,组件要立刻同步更新内部状态保持一致;
3.	View如果要改变Store状态,必须而且只能派发action。

src/views/ControlPanel.js

class ControlPanel extends React.Component{
    render(){
        return(
        	<div>
            	<Counter caption="First" />
                <Counter caption="Second" />
                <Counter caption="Third" />
                <hr/>
                <Summary/>
            </div>
        )
    }
}

src/views/Counter.js

import * as Actions from '../Actions'
import CounterStore from '../stores/CounterStore'

class Counter extends React.Component{
    constructor(props) {
        super(props);
        this.onChange = this.onChange.bind(this);
        this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
        this.onClickDecrementButton = this.onClickDecrementButton.bind(this);
        //1.CounterStore.getCounterValues函数获得了所有计数器的当前值
		//然后把this.state初始化为对应caption字段的值
        this.state = {
          count: CounterStore.getCounterValues()[props.caption]
        }    
    }


    //2.现在,Counter组件中的state是Flux Store上状态的一个同步镜像
    //为了保持两者一致,当CounterStore上状态变化时,Counter组件也要对应变化
    componentDidMount(){
        CounterStore.addChangeListener(this.onChange)
    }
    componentWillUnmount(){
        CounterStore.removeChangeListener(this.onChange)
    }    
    onChange(){
        const newCount =CounterStore.getCounterValues()[this.props.caption]
        this.setState({count: newCount});
    }
    
    //3.只有当状态不一致时才更新状态
    shouldComponentUpdate(nextProps, nextState) {
        return (nextProps.caption !== this.props.caption) ||
               (nextState.count !== this.state.count);
    }
    
	//4.让React组件触发事件时派发action,只需把action对象
    onClickIncrementButton(){
        Actions.increment(this.props.caption)
    }
    onClickDecrementButton(){
        Actions.decrement(this.props.caption)
    }
    
    render(){
        const {caption} =this.props
        return(
        	<div>
            	<input type="button" onClick={this.onClickIncrementButton} value="+"/>
                <input type="button" onClick={this.onClickDecrementButton} value="-"/>
                <span>{caption} count:{this.state.count}</span>
            </div>
        )
    }
}

可见,在Counter组件中有两处用到CounterStore的getCounterValues函数,
第一处是在构造函数中初始化this.state的时候
第二处是在响应CounterStore状态变化的onChange函数中
同样一个Store的状态,为了转换为React组件的状态,有两次重复的调用

src/views/Summary.js

import SummaryStore from '../stores/SummaryStore'
class Counter extends React.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(
        	<div>
            	total:{this.state.sum}
            </div>
        )
    }
}

总结

  • 引入Dispatcher对象
  • 定义action类型
  • 定义action构造函数
  • view中触发事件以派发action
  • 编写store状态
    • 定义默认状态值
    • 定义变量以让其它地方获取当前状态
    • 定义监听
    • 注册一个回调,接受派发过来的action对象为参数。接着通过传过来的对象更新store状态;再更新监听(示例中是emitChange),然后调用者通过addChangeListner监听某store的变化,如果有变化,就调用传进来的回调(示例中是onChange),将store中新的状态渲染到视图中

Flux的架构下,应用的状态被放在了Store中,React组件只是扮演View的作用,被动根据Store的状态来渲染。在上面的例子中,React组件依然有自己的状态,但是已经完全沦为Store组件的一个映射,而不是主动变化的数据。

用户的操作引发的是一个“动作”的派发,这个派发的动作会发送给所有的Store对象,引起Store对象的状态改变,而不是直接引发组件的状态改变。

Flux带来了哪些好处呢?最重要的就是“单向数据流”的管理方式。

在Flux中,如果要改变界面,必须改变Store中的状态,如果要改变Store中的状态,必须派发一个action对象

Flux的不足

Store之间依赖关系

如果两个Store之间有逻辑依赖关系,就必须用上Dispatcher的waitFor函数。SummaryStore对action类型的处理,依赖于CounterStore已经处理过了。

SummaryStore靠register函数的返回值dispatchToken标识CounterStore,而dispatchToken的产生,当然是CounterStore控制的,即:

  • CounterStore必须要把注册回调函数时产生的dispatchToken公之于众
  • SummaryStore必须要在代码里建立对CounterStore的dispatchToken的依赖

难以进行服务器端渲染

在Flux的体系中,有一个全局的Dispatcher,然后每一个Store都是一个全局唯一的对象,这对于浏览器端应用完全没有问题,但是如果放在服务器端,就会有大问题。

和一个浏览器网页只服务于一个用户不同,在服务器端要同时接受很多用户的请求,如果每个Store都是全局唯一的对象,那不同请求的状态肯定就乱套了。

Store混杂了逻辑和状态

Store封装了数据和处理数据的逻辑。但是,当我们需要动态替换一个Store的逻辑时,只能把这个Store整体替换掉,那也就无法保持Store中存储的状态。

还有一些应用,在生产环境下就要根据用户属性来动态加载不同的模块,而且动态加载模块还希望不要网页重新加载,这时候也希望能够在不修改应用状态的前提下重新加载应用逻辑,这就是热加载(Hot Load)

posted @ 2020-10-03 11:39  sanhuamao  阅读(873)  评论(0编辑  收藏  举报