从flux到redux
- 2019 年 12 月 5 日
- 笔记
从flux到redux
flux既是一个前端架构,更是一种状态管理的思想。
2013年,Facebook公司让React亮相的同时,也推出了Flux框架,React和Flux相辅相成,Facebook认为两者结合在一起才能构建大型的JavaScript应用。
MVC的缺陷
谈起MVC,我们想到的传统MVC架构可能是这样的:

但是,Facebook的工程部门在应用MVC架构时逐渐发现,对于非常巨大的代码库和庞大的组织,“MVC真的很快就变得非常复杂”。每当工程师想要增加一个新的功能时,对代码的修改很容易引入新的bug,因为不同模块之间的依赖关系让系统变得“脆弱而且不可预测”。对于刚刚加入团队的新手,更是举步维艰。

Facebook工程师眼中MVC的画风
传统的MVC框架,为了让数据流可控,Controller应该是中心,当View要传递消息给Model时,应该调用Controller的方法,同样,当Model要更新View时,也应该通过Controller引发新的渲染。
而浏览器端MVC框架,存在用户的交互处理,界面渲染出来之后,Model和View依然存在于浏览器中,这时候就会诱惑开发者为了简便,让现存的Model和View直接对话。
所以Facebook提出了他们眼中的解决方案,那就是flux。

•Dispatcher,处理动作分发,维持Store之间的依赖关系;•Store,负责存储数据和处理数据相关逻辑;•Action,驱动Dispatcher的JavaScript对象;•View,视图部分,负责显示用户界面。
如果非要把Flux和MVC做一个结构对比,那么,Flux的Dispatcher相当于MVC的Controller,Flux的Store相当于MVC的Model,Flux的View当然就对应MVC的View了,至于多出来的这个Action,可以理解为对应给MVC框架的用户请求。
在MVC框架中,系统能够提供什么样的服务,通过Controller暴露函数来实现。每增加一个功能,Controller往往就要增加一个函数;在Flux的世界里,新增加功能并不需要Dispatcher增加新的函数,实际上,Dispatcher自始至终只需要暴露一个函数Dispatch,当需要增加新的功能时,要做的是增加一种新的Action类型,Dispatcher的对外接口并不用改变。当需要扩充应用所能处理的“请求”时,MVC方法就需要增加新的Controller,而对于Flux则只是增加新的Action。
在react中使用flux
现在用flux重构上篇文章创造的计数器。
npm i flux -S
页面有三个参与计算的clickCounter组件,大致的思路是:每个组件都根据label获取对应的状态。
Dispatch
在src下创建Dispatcher:
// src/Dispatcher/index.js import {Dispatcher} from 'flux'; export default new Dispatcher();
在这里,我们引入了dispatch类,然后创造一个新的对象作为这个文件的默认输出就足够了。在其他代码中,将会引用这个全局唯一的Dispatcher对象。
Dispatcher存在的作用,就是用来派发action,接下来我们就来定义应用中涉及的action。
Action
action顾名思义代表一个“动作”,不过这个动作只是一个普通的JavaScript对象,代表一个动作的纯数据,类似于DOM API中的事件(event)。甚至,和事件相比,action其实还是更加纯粹的数据对象,因为事件往往还包含一些方法,比如点击事件就有preventDefault方法,但是action对象不自带方法,就是纯粹的数据。
作为管理,action对象必须有一个名为type的字段,代表这个action对象的类型,为了记录日志和debug方便,这个type应该是字符串类型。定义action通常需要两个文件:
•第一个文件(src/Action/ActionTypes.js)定义action的类型:type:
export default { INCREMENT:'increment', DECREMENT:'decrement' }
•第二个文件(src/Action/index.js)放构造函数,又名ActionCreator,它通过ActionTypes
对象向store提交"事件"请求,这些事件名就是actionTypes内定义的名称:
import ActionTypes from './ActionTypes'; import Dispatcher from '../Dispatcher' const increment=(label)=>{ Dispatcher.dispatch({ type:ActionTypes.INCREMENT, label }) } const decrement=(label)=>{ Dispatcher.dispatch({ type:ActionTypes.DECREMENT, label }) } export default { increment, decrement }
这个index.js导出了两个action构造函数increment和decrement,当这两个函数被调用的时候,创造了对应的action对象,并立即通过AppDispatcher.dispatch函数派发出去。
store
一个Store也是一个对象,这个对象存储应用状态,同时还要接受Dispatcher派发的动作,根据动作来决定是否要更新应用状态。同时,它还可以被用来获取状态,并不一定非要储存状态。
counterStore
首先关注子组件计数器的状态,在src/store/CounterStore.js新建:
import Dispatcher from '../Dispatcher'; import ActionTypes from '../Action/ActionTypes'; import { EventEmitter } from 'events'; const CHANGE_EVENT = 'changed'; // 初始状态 const counterValues = { firstCount: 0, secoundCount: 0, thirdCount: 0 } 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(callback) } }); CounterStore.dispatchToken = Dispatcher.register((action) => { if (action.type === ActionTypes.INCREMENT) { // 更新引用对象,再触发changed事件 counterValues[action.label]++; CounterStore.emitChange(); } else if (action.type === ActionTypes.DECREMENT) { counterValues[action.label]--; CounterStore.emitChange(); } }); export default CounterStore;
上述的代码中,我们让CounterStore扩展了EventEmitter.prototype
,等于让CounterStore成了EventEmitter对象,一个EventEmitter实例对象支持下列相关函数。
•emit函数,可以广播一个特定事件,第一个参数是字符串类型的事件名称;•on函数,可以增加一个挂在这个EventEmitter对象特定事件上的处理函数,第一个参数是字符串类型的事件名称,第二个参数是处理函数;•removeListener函数,和on函数做的事情相反,删除挂在这个EventEmitter对象特定事件上的处理函数,和on函数一样,第一个参数是事件名称,第二个参数是处理函数。要注意,如果要调用removeListener函数,就一定要保留对处理函数的引用。
对于CounterStore对象,emitChange、addChangeListener和removeChangeListener函数就是利用EventEmitter上述的三个函数完成对CounterStore状态更新的广播、添加监听函数和删除监听函数等操作。
注册到dispatcher
目前实现的Store只有注册到Dispatcher实例上才能生效。Dispatcher有一个函数叫做register,接受一个回调函数作为参数。返回值是一个token,这个token可以用于Store之间的同步。
// 注册的回调函数包含了业务方法 CounterStore.dispatchToken = Dispatcher.register((action) => { if (action.type === ActionTypes.INCREMENT) { counterValues[action.counterCaption] ++; CounterStore.emitChange(); } else if (action.type === ActionTypes.DECREMENT) { counterValues[action.counterCaption] --; CounterStore.emitChange(); } });
现在我们来仔细看看register接受的这个回调函数参数,这是Flux流程中最核心的部分,当通过register函数把一个回调函数注册到Dispatcher之后,所有派发给Dispatcher的给Dispatcher的action对象,都会传递到这个回调函数中来。
总和计数器(SummaryStore)
总和计数器和counterStore的内容差不多,不同的地方在于就是提供了getSummary方法,此外,需要等待(waiteFor)CounterStore计算完毕后,才计算总和。
import Dispatcher from '../Dispatcher'; import ActionTypes from '../Action/ActionTypes'; import CounterStore from './CounterStore.js'; 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.dispatchToken = Dispatcher.register((action) => { if ((action.type === ActionTypes.INCREMENT) || (action.type === ActionTypes.DECREMENT)) { Dispatcher.waitFor([CounterStore.dispatchToken]); SummaryStore.emitChange(); } }); export default SummaryStore;
可能你会好奇,目前CounterStore已经把所有的状态都储存了,为什么还会有一个SummaryStore?
的确,SummaryStore并没有存储自己的状态,当getSummary被调用时,它是直接从CounterStore里获取状态计算的。CounterStore提供了getCounterValues函数让其他模块能够获得所有计数器的值,SummaryStore也提供了getSummary让其他模块可以获得所有计数器当前值的总和。不过,既然总可以通过CounterStore.getCounterValues函数获取最新鲜的数据,SummaryStore似乎也就没有必要把计数器当前值总和存储到某个变量里。
事实上,可以看到SummaryStore并不像CounterStore一样用一个变量counterValues存储数据,SummaryStore不存储数据,而是每次对getSummary的调用,都实时读取CounterStore.getCounterValues,然后实时计算出总和返回给调用者。可见,虽然名为Store,但并不表示一个Store必须要存储什么东西,Store只是提供获取数据的方法,而Store提供的数据完全可以另一个Store计算得来。
总和计数器关注点在于注册方法用了一个waitFor:
SummaryStore.dispatchToken = AppDispatcher.register((action) => { if ((action.type === ActionTypes.INCREMENT) || (action.type === ActionTypes.DECREMENT)) { AppDispatcher.waitFor([CounterStore.dispatchToken]); SummaryStore.emitChange(); } });
这样我们已经注册了两个store到Disparcher上,派发事件的回调函数的顺序是怎样的呢?
可以认为Dispatcher调用回调函数的顺序完全是无法预期的,不要假设它会按照我们期望的顺序逐个调用。
flux的Dispatcher的waitFor方法。在SummaryStore的回调函数中,之前在CounterStore中注册回调函数时保存下来的dispatchToken终于派上了用场。Dispatcher的waitFor可以接受一个数组作为参数,数组中每个元素都是一个Dispatcher.gister函数的返回结果,也就所谓的dispatchToken。这个waitFor函数告诉Dispatcher,当前的处理必须要暂停,直到dispatchToken代表的那些已注册回调函数执行结束才能继续。
在源码实现上:当一个派发动作发生后,Dispatcher会检查weitFor中的状态回调函数是否被执行了,只有被执行了,才会根据新的状态来计算。因此,即使SummaryStore比CounterStore提前接收到了action对象,在emitChange中调用waitFor,也就能够保证在emitChange函数被调用的时候,CounterStore也已经处理过这个action对象,一切完美解决。
这里要注意一个事实,Dispatcher的register函数,只提供了注册一个回调函数的功能,但却不能让调用者在register时选择只监听某些action,换句话说,每个register的调用者只能这样请求:“当有任何动作被派发时,请调用我。”但不能够这么请求:“当这种类型还有那种类型的动作被派发的时候,请调用我。”
当一个动作被派发的时候,Dispatcher就是简单地把所有注册的回调函数全都调用一遍,至于这个动作是不是对方关心的,Flux的Dispatcher不关心,要求每个回调函数去鉴别。
view
view并不是非得使用react,你可以使用任何喜欢的哪怕是自创的页面框架。但无论是哪个架构,都少不了以下流程:
•创建时要读取Store上状态来初始化组件内部状态;•当Store上状态发生变化时,组件要立刻同步更新内部状态保持一致;•View如果要改变Store状态,必须而且只能派发action。
回到总体的页面,我们不需要再把子组件的计算逻辑放到最上层来了。
import React, { Component } from 'react'; import ClickCounter from './ClickCounter' import AllCount from './AllCount'; const styles={ app:{ width:250, margin:'100px auto' } } class App extends Component { constructor(props) { super(props) } render() { return ( <div style={styles.app}> <ClickCounter label={'firstCount'} /> <ClickCounter label={'secoundCount'}/> <ClickCounter label={'thirdCount'} /> <hr /> <AllCount /> </div> ); } } export default App;
clickCounter组件,需要在不同生命周期更新:
import React, { Component } from 'react' import CounterStore from './stores/CounterStore'; import Actions from './Action' const styles = { counter: { width: 250, display: 'flex' }, showbox: { width: 80, textAlign: 'center' }, label: { width: 120, } } class ClickCounter extends Component { constructor(props) { super(props) 根据label从总体状态中拿到属于自己的`FIRST/SECOUND/THIRD`count this.state = { count: CounterStore.getCounterValues()[props.label] } } componentDidMount() { // 开启监听 CounterStore.addChangeListener(this.onChange); } componentWillUnmount() { // 防止内存泄漏 CounterStore.removeChangeListener(this.onChange); } onChange = () => { const newCount = CounterStore.getCounterValues()[this.props.label]; this.setState({ count: newCount }); } onClickIncrementButton = () => { Actions.increment(this.props.label); } onClickDecrementButton = () => { Actions.decrement(this.props.label); } render() { return ( <div style={styles.counter}> <div style={styles.label}>{this.props.label}</div> <button onClick={this.onClickDecrementButton}>-</button> <div style={styles.showbox}>{this.state.count}</div> <button onClick={this.onClickIncrementButton}>+</button> </div> ) } } export default ClickCounter;
在总和计数器上,就比较简单了:
import React, { Component } from 'react' import SummaryStore from './stores/SummaryStore'; export default class 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 ( <div>Total Count: {this.state.sum}</div> ); } }
flux的优与劣
和纯react的state维护相比,flux架构下的计数器有了明显变化。
回顾一下纯React实现的版本,应用的状态数据只存在于React组件之中,每个组件都要维护驱动自己渲染的状态数据,单个组件的状态还好维护,但是如果多个组件之间的状态有关联,那就麻烦了。
比如ClickCounter组件和AllCount组件,AllCount组件需要维护所有ClickCounter组件计数值的总和,ClickCounter组件和AllCount分别维护自己的状态,如何同步AllCount和ClickCounter状态就成了问题,React只提供了props方法让组件之间通信,组件之间关系稍微复杂一点,这种方式就显得非常笨拙。
Flux的架构下,应用的状态被放在了Store中,React组件只是扮演View的作用,被动根据Store的状态来渲染。在上面的例子中,React组件依然有自己的状态。但是已经沦落为store的一个映射,而不是主动变化的数据。
因此flux的优势可以归结为"单向数据流"。
在Flux的理念里,如果要改变界面,必须改变Store中的状态,如果要改变Store中的状态,必须派发一个action对象,这就是规矩。在这个规矩之下,想要追溯一个应用的逻辑就变得非常容易。
MVC最大的问题就是无法禁绝View和Model之间的直接对话,对应于MVC中View就是Flux中的View,对应于MVC中的Model的就是Flux中的Store,在Flux中,Store只有get方法,没有set方法,根本不可能直接去修改其内部状态,View只能通过get方法获取Store的状态,无法直接去修改状态,如果View想要修改Store状态的话,只有派发一个action对象给Dispatcher。
flux也存在一些缺点:
在Flux的体系中,如果两个Store之间有逻辑依赖关系,就必须用上Dispatcher的waitFor函数。要通过waitFor函数告诉Dispatcher,先让CounterStore处理这些action对象,只有CounterStore搞定之后SummaryStore才继续。那么,SummaryStore如何标识CounterStore呢?靠的是register函数的返回值dispatchToken,而dispatchToken的产生,当然是CounterStore控制的,换句话说,要这样设计:
•CounterStore必须要把注册回调函数时产生的dispatchToken公之于众;•SummaryStore必须要在代码里建立对CounterStore的dispatchToken的依赖。
此外flux也难以在服务端进行渲染。