上一篇分析了Flux出现的背景和原理,最核心的思想就是“组件化+单向数据流”。
但是,Flux在设计上并非完美,具体来说主要存在以下2个不足:
1. 多Store数据依赖
由于Flux采用多Store设计,各个Store之间可能存在数据依赖。以flux-chat为例:在这个聊天软件里,可能会有多个人给你发消息,比如Dave给你发了3条,Brian给你发了2条,当你点开某个人给你发的消息后,界面需要刷新,显示你目前还有几个人的未读消息没有查看:
为了解决这个需求,创建了3个Store:
- ThreadStore用来存储消息组状态
- MessageStore用来存储每个组里的消息的状态
- UnreadThreadStore用来计算目前还有几个消息组没有查看
当你点开某个消息组时,显然你需要先更新ThreadStore和MessageStore,然后再更新UnreadThreadStore。由于Store的注册顺序是不确定的,为了应付这种依赖,Flux提供了waitFor()机制,每个Store在注册之后都会生成一个令牌(dispatchToken),通过等待令牌的方式确保其他Store被优先更新。
因此UnreadThreadStore的代码会写成下面这个样子:
Dispatcher.waitFor([
ThreadStore.dispatchToken,
MessageStore.dispatchToken
]);
switch (action.type) {
case ActionTypes.CLICK_THREAD:
UnreadThreadStore.emitChange();
break;
...
}
虽然可以工作,但是总觉得不是很优雅,在一个Store中需要显示地包含其他Store的调用。当然你会说,干脆把这3个Store的代码糅到一起,搞成一个Store不就行了?但是这样又会导致代码结构不够清晰,不利于多模块分工协作。
为了兼顾这两个方面,Redux使用全局唯一Store,外部可以使用多个reducer来修改Store的不同部分,最后会把所有reducer的修改再组合成一个新的Store状态。
2.状态修改不是纯函数
所谓纯函数,是指输出只和输入相关,相同的输入一定会得到相同的输出。用专业一点的术语来说,纯函数没有“副作用”。我们先来看看Flux中是怎么修改状态的:
Dispatcher.register(action => {
switch(action.type) {
case ActionTypes.CLICK_THREAD:
_currentID = action.threadID;
ThreadStore.emitChange();
break;
...
}
可以看到,是直接修改变量值,然后显式发送一个change事件来通知View。
我们再来看看Redux中是怎么修改状态的:
export default function threadReducer(state = {}, action) {
switch (action.type) {
case ActionTypes.CLICK_THREAD: {
return { ...state, _currentID: action.threadID };
...
}
细心的人可能已经看出来了,主要有3点区别:
- 前面的函数里只有一个action参数,而这里多了一个state参数
- 不是直接修改state中的字段,而是需要返回一个新的state对象
- 不需要显式发送事件通知View,实际上,Redux内部会检测state对象的引用是否发生了变化,然后自动通知View进行刷新
那么有人会说了,为啥要这么做,好像也没看到啥好处嘛?当然是有好处的,这样可以支持“时间旅行调试(Time Travel Debugging)”。所谓时间旅行调试,指的是可以支持状态的无限undo / redo。由于state对象是被整体替换的