深入浅出React+Redux(三:Flux单向数据流,相关代码在github flux分支)

前言

通过上章,我们能感觉到仅仅通过prop和state 管理React大型项目,简直是个巨大,恐怖乃至不可完成的挑战。因为社区和个人喜爱还是推荐Redux做项目的状态管理。但是作为单向数据流鼻祖的Flux,也是读者需要整理下区别的。

(一)前端MVC 框架的缺陷

附上一篇文章(需要): 为什么说客户端MVC已死

MVC框架是业界广泛接受的一种前端应用架构类型,它把应用分成三个部分:

  • Model (模型)负责管理数据 ,大部分业务逻辑也应该放在 Model 中;
  • View (视图)负责渲染用户界面,应该避免在 View 中涉及业务逻辑;
  • Controller (控制器)负责接受用户输入 根据用户输入调用对应的 Model 部分逻辑,把产生的数据结果交给 View 部分,让 View 渲染出必要的输出 。

MVC几个部分组成部分和请求的关系图

深入浅出React+Redux(三:Flux单向数据流,相关代码在github flux分支)_第1张图片

这种将一个应用划分为多个组件,就是“分而治之”。但是现实项目足够大后,实际情况是什么样子的呢。如下图

深入浅出React+Redux(三:Flux单向数据流,相关代码在github flux分支)_第2张图片

实际上。这是客户端区分服务器的地方,在服务器mvc依旧是霸主地位,它的一个完整请求是以Controller中的request发起,response结束(其实本身数据流也类似单向)·。

但是客户端,总是允许View 和Model 可以直接通信。就会造成上面图中的情况。

(二)FlUX基本概念

Facebook使用 Flux 框架来代替原有的 MVC 框架,他们提出的 Flux 框架大致结构如下图。

深入浅出React+Redux(三:Flux单向数据流,相关代码在github flux分支)_第3张图片

首先,Flux将一个应用分成四个部分。

  1. Dispatcher ,处理动作分发,维持 Store 之间的依赖关系;
  2. Store ,负责存储数据和处理数据相关逻辑 ;
  3. Action ,驱动 Dispatcher 的 JavaScript 对象;
  4. View ,视图部分,负责显示用户界面。

Flux和MVC对比

Flux 的 Dispatcher 相当于 MVC 的Controller, Flux 的 Store 相当于 MVC 的 Model, Flux 的 View 当然就对应 MVC 的 View了,至于多出来的这个 Action ,可以理解为对应给 MVC 框架的用户请求 。

当需要扩充应用所能处理的“请求”时, MVC 方法就需要增加新的 Controller,而对于 Flux 则只是增加新的 Action

(三)FlUX 简单demo

安装依赖

$ yarn add flux --dev

(1)Dispatcher

首先,我们要创造一个 Dispatcher,

src/appDispatcher/index.js。创造这个唯一 的Dispatcher 对象

/**
 * @component appDispatcher
 * @description 全局Dispatcher
 * @time 2018/1/16
 * @author jokerXu
 */
import {Dispatcher} from 'flux';

export default new Dispatcher();

Dispatcher 存在的作用,就是用来派发 action ,接下来我们就来定义应用中涉及的 action。

(2)Action

action 顾名思义代表一个“动作”,不过这个动作只是一个普通的 JavaScript 对象,代表一个动作的纯数据,类似于 DOM API 中的事件( event ) 。 甚至,和事件相比, action其实还是更加纯粹的数据对象,因为事件往往还包含一些方法,比如点击事件就有
preventDefault 方法,但是 action 对象不自带方法,就是纯粹的数据 。

作为管理, action 对象必须有一个名为 type 的字段,代表这个 action 对象的类型,
为了记录日志和 debug 方便,这个 type 应该是字符串类型 。

定义 action 通常需要两个文件,一个定义 action 的类型,一个定义 action 的构造函
数(也称为 action creator ) 。 分成两个文件的主要原因是在 Store 中会根据 action 类型做
不同操作,也就有单独导人 action 类型的需要 。

首先在src/actionTypes/demo.js中定义类型。

/**
 * @component actionTypes
 * @description demo动作类型
 * @time 2018/1/22
 * @author ***
 */

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

上面代码表示,执行两次操作,一个事点击”+”,一个事点击”-“。

然后在src/actions/demo.js中定义动作

/**
 * @component actions
 * @description demo动作
 * @time 2018/1/22
 * @author ***
 */

import * as ActionTypes from '../actionTypes/demo';
import AppDispatcher from '../appDispatcher';

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


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

虽然出于业界习惯,这个文件被命名为 Actions. ,但是要注意里面定义的并不是
action 对象本身,而是能够产生并派发 action 对象的函数 。

我们导出了两个 action 构造函数 increment 和 decrement,当这两个函数被调
用的时候,创造了对应的 action 对象,并立即通过 AppDispatcher.dispatch 函数派发出去 。

(3)Store

一个 Store 也是一个对象,这个对象存储应用状态,同时还要接受 Dispatcher 派发的
动作,根据动作来决定是否要更新应用状态 。

我们创造两个 Store ,一个是为 Counter 组件服务的 CounterStore ,另 一个就是为总
数服务的 SummaryStore 。

(1)定义src/stores/counterStore.js

/**
 * @component stores
 * @description demo的数量store
 * @time 2018/1/22
 * @author jokerXu
 */
import * as ActionTypes from '../actionTypes/demo';
import {EventEmitter} from 'events';

const CHANGE_EVENT = 'changed';

const counterValues = {
    'First': 0,
    'Second': 10,
    'Third': 20,
};

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);
    }
});

export default CounterStore;

当 Store 的状态发生变化的时候, 需要通知应用的其他部分做必要的响应 。 在我们
的应用中,做出响应的部分当然就是 View 部分,但是我们不应该硬编码这种联系,应
该用消息的方式建立 Store 和 View 的联系 。 这就是为什么我们让 CounterStore 扩展了
EventEmitter.prototype ,等于让 CounterStore 成了 EventEmitter 对象, 一个 EventEmitter
实例对象支持下列相关函数 。

  • emit 函数,可以广播一个特定事件,第一个参数是字符串类型的事件名称 ;
  • on 函数,可以增加一个挂在这个 EventEmitter 对象特定事件上的处理函数,第一个参数是字符串类型的事件名称,第二个参数是处理函数;
  • removeListener 函数, 和 on 函数做的事情相反, 删除挂在这个 EventEmitter 对象特定事件上的处理函数,和 on 函数一样, 第一个参数是事件名称 ,第二个参数是处理函数 。

对于 CounterStore 对象, emitChange 、 addChangeListener 和 removeChangeListener 函
数就是利用 EventEmitter 上述的三个函数完成对 CounterStore 状态更新的广播、添加监昕
函数和删除监昕 函数等操作 。

CounterStore 函 数还提供一个 getCounterValues 函数,用于让应用中其他模块可以读
取当前的计数值,当前的计数值存储在文件模块级的变量 counterValues 中 。

接下来将 Store 只有注册到 Dispatcher 实例上才能真正发挥作用

import AppDispatcher from '../appDispatcher';

CounterStore.dispatchToken = AppDispatcher.register((action) => {
    if (action.type === ActionTypes.INCREMENT) {
        counterValues[action.value]++;
        CounterStore.emitChange();
    } else if (action.type === ActionTypes.DECREMENT) {
        counterValues[action.value]--;
        CounterStore.emitChange();
    }
});

这是最重要 的 一 个 步 骤, 要 把 CounterStore 注 册到全局唯 一 的 Dispatcher 上 去。Dispatcher 有一个函数叫做 register ,接受一个回调函数作为参数 。 返回值是一个 token ,这个 token 可以用于 Store 之间的同步。

现在我们来仔细看看 register 接受的这个回调函数参数,这是 Flux 流程中最核心的部分,当通过 register 函数把一个回调函数注册到 Dispatcher 之后 , 所有派发给 Dispatcher的 action 对象 ,都会传递到这个回调函数中来 。

比如通过 Dispatcher 派发一个动作,代码如下:

AppDispatcher.dispatch ({
    type: ActionTypes.INCREMENT,
    counterCaption: 'First '
});

根据不同的 type ,会有不同的操作,所以注册的回调函数很自然有一个模式,就是
函数体是一串 if-else 条件语句或者 switch 条件语句,而条件语句的跳转条件,都是针对
参数 action 对象的 type 字段

(2)定义src/stores/summaryStore.js

SummaryStore 也有 emitChange 、 addChangeListener 还有 removeChangeListener 函数,
功能一样也是用于通知监昕者状态变化,这几个函数的代码和 CounterStore 中完全重复,
不同点是对获取状态函数的定义,

/**
 * @component stores
 * @description demo的总数store
 * @time 2018/1/22
 * @author jokerXu
 */

import CounterStore from './counterStore';

import * as ActionTypes from '../actionTypes/demo';
import {EventEmitter} from 'events';

const CHANGE_EVENT = 'changed';

function computeSumrnary(counterValues) {
    let summary = 0;
    for (const key in counterValues) {
        if (counterValues.hasOwnProperty(key)) {
            summary += counterValues[key];
        }
    }
    return summary;
}

const SumrnaryStore = Object.assign({}, EventEmitter.prototype, {
    getSummary: function () {
        return computeSumrnary(CounterStore.getCounterValues());
    },
    emitChange: function () {
        this.emit(CHANGE_EVENT);
    },
    addChangeListener: function (callback) {
        this.on(CHANGE_EVENT, callback)
    },
    removeChangeListener: function (callback) {
        this.removeListener(CHANGE_EVENT, callback);
    }
});

export default SumrnaryStore;

可以注意到, SummaryStore 并没有存储自己的状态,当 getSummary 被调用时,它
是直接从 CounterStore 里获取状态计算的 。

可见,虽然名为 Store ,但并不表示一个 Store 必须要存储什么东西, Store 只是提供
获取数据的方法,而 Store 提供的数据完全可以另一个 Store 计算得来 。

SummaryStore 在 Dispatcher 上注册的回调函数也和 CounterStore 很不一样,代码
如下:

import AppDispatcher from '../appDispatcher';

SumrnaryStore.dispatchToken = AppDispatcher.register((action) => {
    if ((action.type === ActionTypes.INCREMENT) ||
        (action.type === ActionTypes.DECREMENT)
    ) {
        AppDispatcher.waitFor([CounterStore.dispatchToken]);
        SumrnaryStore.emitChange();
    }
});

这里使用了 waitFor 函数,这个函数解决的是下面描述的问题。

即使 Flux 按照 register 调用的顺序去调用各个回调函数,我们也完全无法把握各个Store 哪个先装载从而调用 register 函数 。 所以,可以认为 Dispatcher 调用回调函数的顺序完全是无法预期的,不要假设它会按照我们期望的顺序逐个调用 。

Dispatcher 的 waitFor 可以接受一个数组作为参数,数组中每个元素都是一个 Dispatcherregister 函数的返回结果,也就所谓的 dispatchToken 。 这个waitFor 函数告诉 Dispatcher,
当前的处理必须要暂停,直到 dispatchToken 代表的那些已注册回调函数执行结束才能继续 。

注意

Dispatcher 的 register 函数,只提供了注册一个回调函数的功能,但却不能让调用者在 register 时选择只监听某些 action,换句话说,每个 register 的调用者只能这样请求:“ 当有任何动作被派发时,请调用我 。 ”但不能够这么请求:“当这种类型还有那种类型的动作被派发的时候,请调用我 。 ”

当一个动作被派发的时候, Dispatcher 就是简单地把所有注册的回调函数全都调用一
遍,至于这个动作是不是对方关心的, Flux 的 Dispatcher 不关心,要求每个回调函数去
鉴别 。

看起来,这似乎是一种浪费,但是这个设计让 Flux 的 Dispatcher 逻辑最简单化,
Dispatcher 的责任越简单,就越不会出现问题 。 毕竟,由回调函数全权决定如何处理 action
对象,也是非常合理的 。

(4)Views

存在于 Flux 框架中的 React 组件需要实现以下几个功能:

  • 创建时要读取 Store 上状态来初始化组件内部状态;
  • 当 Store 上状态发生变化时,组件要立刻同步更新内部状态保持一致;
  • View 如果要改变 Store 状态,必须而且只能派发 action 。

新增src/views/ControlPanel/Summary.js

/**
 * @component Summary
 * @description
 * @time 2018/1/22
 * @author ***
 */

import React, { Component } from 'react';

import SummaryStore from '../../stores/summaryStore.jsx';

class Summary extends Component {

    constructor(props) {
        super(props);
        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;

只要 CounterStore 发生变化, Counter 组件的 onChange 函数就会被调用。与 componentDidMount 函数中监昕事件相对应,在componentWillUnmount 函数中删除了这个监昕 。

修改src/views/ControlPanel/index.js

import React, { Component } from 'react';
import Counter from './Counter';
import Summary from './Summary';

class ControlPanel extends Component {

    constructor (props) {
        super(props);
        this.initValues = [
            {
                title: 'First',
            },{
                title: 'Second',
            },{
                title: 'Third',
            }
        ];
    }

    // 遍历子组件
    mapCounter=() =>{
        return this.initValues.map((value,index)=>{
            return index} caption={value.title} />;
        })
    };

    render() {
        return (
            <div>
                {this.mapCounter()}
                
div> ) } } export default ControlPanel;

修改src/views/ControlPanel/Counter.jsx

/**
 * @component Counter
 * @description 子组件
 * @time 2018/1/15
 * @author ***
 */
import React, { Component } from 'react';
import PropTypes from 'prop-types';

import CounterStore from '../../stores/counterStore';
import * as Actions from '../../actions/demo';

class Counter extends Component{

    constructor(props){
        super(props);

        this.state= {
            count: CounterStore.getCounterValues() [props.caption],
        }
    }
    clickIncrementHandler=() =>{
        Actions.increment(this.props.caption);
    };
    clickDecrementHandler=() =>{
        Actions.decrement(this.props.caption);
    };
    componentDidMount( ) {
        CounterStore.addChangeListener(this.onChange) ;
    }
    componentWillUnmount() {
        CounterStore.removeChangeListener(this.onChange);
    }
    onChange = () => {
        const newCount = CounterStore.getCounterValues()[this.props.caption];
        this.setState({count: newCount});
    }
    shouldComponentUpdate(nextProps , nextState) {
        return (nextProps.caption !== this.props.caption) || (nextState.count !==this.state.count);
    }

    render(){
        const buttonStyle= {
            marginRight: '15px',
        };
        const { caption }= this.props;
        const { count }= this.state;
        return (
            
{caption} count: {count}
); } } Counter.propTypes= { caption: PropTypes.string.isRequired, }; export default Counter;

(四)Flux 的优势

回顾一下完全只使用 React 实现的版本,应用的状态数据只存在于 React 组件之中,每个组件都要维护驱动自己渲染的状态数据,单个组件的状态还好维护,但是如果多个组件之间的状态有关联,那就麻烦了 。 比如 Counter 组件和 Summary 组件, Summary 组件需要维护所有 Counter 组件计数值的总和, Counter 组件和 Summary 分别维护自己的状态,如何同步 Summary 和 Counter 状态就成了问题, React 只提供了 props 方法让组件之间通信,组件之间关系稍微复杂一点,这种方式就显得非常笨拙 。

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

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

在 Flux 的理念里,如果要改变界面,必须改变 Store 中的状态,如果要改变 Store 中
的状态,必须派发一个 action 对象,这就是规矩 。 在这个规矩之下,想要追溯一个应用
的逻辑就变得非常容易 。

我们已经讨论过 MVC 框架的缺点 MVC 最大的问题就是无法禁绝 View 和 Model 之
间的直接对话,对应于 MVC 中 View 就是 Flux 中的 View ,对应于 MVC 中的 Model 的就
是 Flux 中的 Store ,在 Flux 中, Store 只有 get 方法,没有 set 方法,根本可能直接去修改
其内部状态, View 只能通过 get 方法获取 Store 的状态,无法直接去修改状态,如果 View
想要修改 Store 状态的话,只有派发一个 action 对象给 Dispatcher。这看起来是一个“限制”,但却是一个很好的“限制”,禁绝了数据流泪乱的可能 。简单说来,在 Flux 的体系下,驱动界面改变始于一个动作的派发,别无他法 。

(四)Flux 的不足

(1). Store 之间依赖关系

在 Flux 的体系中,如果两个 Store 之间有逻辑依赖关系,就必须用上 Dispatcher 的
waitFor 函数 。 而 dispatchToken 的产生,当然是 CounterStore 控制的,换句话说,要这样设计:

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

虽然 Flux 这个设计的确解决了 Store 之间的依赖关系,但是,这样明显的模块之间
的依赖,看着还是让人感觉不大舒服,毕竟,最好的依赖管理是根本不让依赖产生 。

(2). 难以进行服务器端渲染

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

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

并不是说 Flux 不能做服务器端渲染,只是说让 Flux 做服务器端渲染很困难,实际
上, Facebook 也说的很清楚, Flux 不是设计用作服务器端渲染的,他们也从来没有尝试
过把 Flux 应用于服务器端。

(3). Store 混杂了逻辑和状态

Store 封装了数据和处理数据的逻辑,用面向对象的思维来看,这是一件好事,毕
竟对象就是这样定义的 。 但是,当我们需要动态替换一个 Store 的逻辑时,只能把这个
Store 整体替换掉,那也就无法保持 Store 中存储的状态 。

最后

我们把 Flux 看作一个框架理念的话, Redux 是 Flux 的一种实现,除了 Redux 之外,
还有很多实现 Flux 的框架,比如 Reflux, Fluxible 等,毫无疑问 Redux 获得的关注最多,
这不是偶然的,因为 Redux 有很多其他框架无法比拟的优势 。

你可能感兴趣的:(React,React,flux)