前端数据流管理

react 的范式为 UI = render(state),用户的界面完全取决于数据层。react 中通过管理状态(state)来实现对组件的管理,当 state 发生变更后,react 就会重新渲染界面,组件与组件之间也经常需要共享状态。如果缺乏一个好的状态管理方案,那么共享数据将变得麻烦,同时状态不受控的话会让我们很难跟踪调试程序。

react 本身采用的时自上而下的单向组件数据流,我们通常将代码抽成 Smart Component 组件和 Dumb Component 组件。通过 proprs 来连接,来完成功能。针对小的项目,这就足够了,但是项目大了以后会出现如下几个问题:

  1. 如何跨组件实现状态同步
    react 16 提供新的的 context 可以解决这一问题,但是 context 一般会放顶级组件上,一旦有改变将触发所有组件的re-render,这将带来损耗。
  2. 如何让状态变得可预知,甚至可回溯
  3. 如何避免组件臃肿,Model 和 View 都混在了一起

为此前端提出了一个通用解决思路:把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。针对这个思路,我们来看下都有哪些实现。 (隔离变化,约定大于配置,其实不管是 Vue,还是 React 都对其状态管理有着同样的要求)

单向数据流体系

Store 模式

我们将状态存在一个全局变量 store 里面,store 里面设置一些方法来控制 state 的改变。
约定:组件只能通过调用 store 上的方法来改变数据,而不能直接操作 store 里面的 state。这样就保证了数据的可追溯。

1
2
3
4
5
6
7
8
9
10
11
let store = {
state: {
data: {}
},
setDataAction(newValue) {
this.state.data = newValue;
},
clearDataAction() {
this.state.data = {};
}
}

进一步,为了方便管理 view 层调用相应的 store 方法,我们包装一个 dispatcher 来映射 view 层的一个动作 action 到 store 上。这样一个 flux 架构就诞生了。

Flux

Flux 本身是一种思想,一种约定。Flux 把一个应用分成 4 个部分:View, Action, Dispatcher, Store

a3.png

View 层用来展示数据,当用户操作UI上的某个操作,就会触发 Dispatcher。
Dispatcher 就像一个请求中转站一样会 dispatch 一个 action 给 Store。Store 根据这个 action 来改变数据。当然 Action 也可以由其他地方触发。
一旦 Store 发生了变化,就会往外面发送一个事件,比如 change,来通知所有的订阅者。View 会监听这个事件,从而触发自身的 re-render。(实现:发布订阅模式)
Dispatcher 的作用是接收所有的 Action,然后发给所有的 Store。 Store 的改变只能通过 Action,不能通过其他方式。

改写上面的例子:

store 部分加入发布订阅

1
2
3
4
5
export store = assign((), EventEmitter.prototype, store, {
emitChange: function() {
this.emit('change');
}
})

Dispatcher 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
import store from '../store';
AppDispatcher.regiseter((action) => {
switch(action.type) {
case 'setData':
store.setData(action.data);
// 触发变更
store.emitChange();
break;
case 'clear':
store.clearData();
}
});

View 层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import store from '../store';
import AppDispatcher from '...';
class App extends React.Component {
componentDidMount: () => {
store.addChangeListener(this._onChange);
}
componentWillUnmount: () => {
store.removeChangeListener(this._onChange);
}
_onChange: () => {
this.setState({
items: ListStore.getAll()
});
}
handleClick: () => {
AppDispatcher({
type: 'setData',
data: data
});
}
render() {
// ...
}
}

可以看出,Flux 核心思想就是数据都是单向流动的。
Flux 本身会有很多个 store 来存储引用数据,并在各自的 store 里面执行更新逻辑。那么当多个store之间有依赖关系的时候,就不太好处理。同时 store 里面不仅封装了数据,还有处理数据的逻辑。

Redux

在 Flux 的基础上,Redux 对其进行了一些改进增强。

a4.png

在 Redux 中没有 Dispatcher 的概念,它使用 reducer 来进行事件处理。
reducer 是一个纯函数,每个 reducer 负责维护应用整体 state 树中的某一部分,多个 reducer 可以通过 combineReducers 方法合成一个根reducer,这个根reducer负责维护完整的 state。
约定: reducer 必须为纯函数(此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O 设备产生的外部输出无关,例如 Ajax)。
由于有这样的规定,redux 成功副作用隔离。我们很容易判断出数据的变化的原因,数据流清晰可回朔。同时 Redux 引入的 immutable 进一步隔离了对象引用的问题。

对比 Flux

Redux 和 Flux 之间最大的区别就是对 store/reducer 的抽象,Flux 中 store 是各自为战的,每个 store 只对对应的 controller-view 负责,每次更新都只通知对应的 controller-view;而 Redux 中各子 reducer 都是由根reducer统一管理的,每个子reducer的变化都要经过根reducer的整合。

a5.png

综上:Redux有三大原则:

  1. 单一数据源:Flux 的数据源可以是多个。
  2. State 是只读的:Flux 的 State 可以随便改。
  3. 使用纯函数来执行修改:Flux 执行修改的不一定是纯函数。

Redux 异步

在 Redux 中,每当我们发出一个 Action,Reducer 就会立即算出 State。那么我们想支持异步呢?在哪儿加入异步操作呢?
Reducer ? 纯函数,不能引入IO,不适合。Action ? 一个纯对象,没有位置。所以只能通过包装 dispatch 加上中间件的动能。例如:

1
2
3
4
5
6
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}

Redux 提供了一个 applyMiddleware 方法来应用中间件:

1
2
3
4
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);

这个方法主要就是把所有的中间件组成一个数组,依次执行。也就是说,任何被发送到 store 的 action 现在都会经过thunk,promise,logger 这几个中间件了。

关于 Redux 的异步处理,之后会有单独解析 redux-sage 的内容。

Mobx

之前说的都是单项数据流方案,思想主要就是函数是编程(FP)的思想。Mobx 则是一个 TFRP 的框架,FRP 的一个分支。

flow.png

mobx的流程图如上,通常是:触发action,在action中修改state,通过computed拿到state的计算值,自动触发对应的reactions,这里包含autorun,渲染视图等。有一点需要注意:相对于react来说,mobx没有一个全局的状态树,状态分散在各个独立的 store 中。这种自动订阅,自动发布的模式,使得开发十分方便。
但是相对的,mobx 中并没有解决副作用问题,同时,对 props 的直接修改,也会导致与 react 对 props 的不可变定义冲突。因此 mobx 后来给出了 action 解决方案,解决了与 react props 的冲突,但是没有解决副作用未强制分离的问题。

对比一下:

  1. redux 采用全局单一 store,mobx 则由多个独立 store 组成
  2. redux 通过 action 将副作用隔离在 reducer 之外。而 mobx 比较自由,没有对副作用进行处理。
  3. redux 函数式、不可变、模式化;mobx 响应式、依赖追踪
  4. redux 开发需要些很多样板代码,但是调试数据的时候确很方便。mobx 书写简单,但是没有强约束换来的是调试困难。

参考资料