通过上章,我们能感觉到仅仅通过prop和state 管理React大型项目,简直是个巨大,恐怖乃至不可完成的挑战。因为社区和个人喜爱还是推荐Redux做项目的状态管理。但是作为单向数据流鼻祖的Flux,也是读者需要整理下区别的。
附上一篇文章(需要): 为什么说客户端MVC已死
MVC框架是业界广泛接受的一种前端应用架构类型,它把应用分成三个部分:
MVC几个部分组成部分和请求的关系图
这种将一个应用划分为多个组件,就是“分而治之”。但是现实项目足够大后,实际情况是什么样子的呢。如下图
实际上。这是客户端区分服务器的地方,在服务器mvc依旧是霸主地位,它的一个完整请求是以Controller中的request发起,response结束(其实本身数据流也类似单向)·。
但是客户端,总是允许View 和Model 可以直接通信
。就会造成上面图中的情况。
Facebook使用 Flux 框架来代替原有的 MVC 框架,他们提出的 Flux 框架大致结构如下图。
首先,Flux将一个应用分成四个部分。
Flux和MVC对比
Flux 的 Dispatcher 相当于 MVC 的Controller, Flux 的 Store 相当于 MVC 的 Model, Flux 的 View 当然就对应 MVC 的 View了,至于多出来的这个 Action ,可以理解为对应给 MVC 框架的用户请求 。
当需要扩充应用所能处理的“请求”时, MVC 方法就需要增加新的 Controller
,而对于 Flux 则只是增加新的 Action
。
安装依赖
$ 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
实例对象支持下列相关函数 。
对于 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 组件需要实现以下几个功能:
新增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;
回顾一下完全只使用 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 的体系下,驱动界面改变始于一个动作的派发,别无他法 。
(1). Store 之间依赖关系
在 Flux 的体系中,如果两个 Store 之间有逻辑依赖关系,就必须用上 Dispatcher 的
waitFor 函数 。 而 dispatchToken 的产生,当然是 CounterStore 控制的,换句话说,要这样设计:
虽然 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 有很多其他框架无法比拟的优势 。