笔记,参考程墨老师的《深入浅出React和Redux》
Flux概念
- Dispatcher,处理动作分发,维持Store之间的依赖关系
- Store,负责存储数据和处理数据相关逻辑
- Action,提供给Dispatcher,传递数据给Store
- View,视图部分,负责显示用户界面
首先会产生一个事件,一般是由用户对界面执行的一个操作,Action得到这个操作后,将其交给Dispatcher,再由Dispatcher来进行分发给Store, Store收到通知后对相关数据进行维护,再发出一个更改通知,告诉视图层需要更新View了,然后重新从Store中检索数据,调用setState方法对View进行更新。
Flux应用
示例
安装
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)