State Management
如果你维护过一些不太好的 Angular 项目,会发现,代码的逻辑都柔和到了 Component 中,如果 Component 又没有很好的拆分,一堆 copy paste 的代码。比如以下代码:
// Template 简单的显示 user
{{user | json}}
onChangeUser(id: string) {
// Call API to change user
}
onRefreshUser() {
// Call API to change user
}
...
...
各种其他 Action
这样,我们就需要穿梭在各种方法里面,看数据到底是怎么修改的,还有理清楚各个方法之间的调用关系,非常痛苦。
这才有了所谓的单向数据流
的概念,就是将数据状态的修改抽离开来,在 View 层,也就是 component 层,永远只做两件事:
- 读取当前 state
- 告诉 state management 如何修改 state
这样,就可以帮助我们的 component 从各种复杂的业务逻辑中抽离开来,只关心如何去显示数据,也就是所谓的 view 层。
我们一般把状态管理这层,称为一个 Store
那么问题来了,State Management 关 RxJS 啥事?如果你熟悉 React,会知道,React 中有一个套路叫做,ContainerComponent 和 PresentaionComponent,一般由 ContainerComponent 包裹住 state,然后将数据传递给 PresentaionComponent。
为什么会这样呢?
因为如果直接在 Component 中使用 state,当 State 发生变化以后,我们没有办法通知 Component 数据变化了,只能通过 ContainerComponent 这一层包裹,将相应的数据拆分成不同的 props,这样就可以通过 component 的 props 通知 child component 数据已经发生变化了。
然而,讲到通知数据变化,你可能就明白了,这是 RxJS 的特长。如果通过 RxJS 来实现 store, 就可以避免调复杂的 ContainerComponent, 简化 Redux 的学习门槛。(当然,现在 React 可以通过 Hooks 来实现类似的功能,有空我们可以聊以下 Hooks 跟 RxJS 的关系)
如何实现 State Management
首先,我们看一下,Store 是什么?
为了实现单向数据流,我们需要 store 满足以下功能:
- 通过 Store 能获得 state,并且当 state 发生变化以后,能够有办法通知。
- 通过 Store 能够修改 state,但是不能直接修改 state,只能告诉 store 想要做什么样的修改,具体的业务逻辑,应该由 Store 来处理。
如果有一些 RxJS 的背景知识的话,会发现,BehaviorSubject
来实现 state
是非常合适的。
下面我们通过 BehaviorSubject
实现一个简单的 state
接口:
class Store {
state$: Observable<State>;
private _state$: BehaviorSubject<State>;
constructor(initialState: State) {
this._state$ = new BehaviorSubject<State>(initialState);
this.state$ = this._state$.asObservable();
}
get state() {
return this._state$.value;
}
}
- state 可以获得最新的 state 值
- state$ 可以在 state 发生变化的时候 emit changes
- 而且,外部无法直接修改 state
那么问题来了,我们该如何修改 state 呢?我们可以定义一个 dispacth 方法,永远只传入 Action 名字,来告诉 store 需要触发相应的修改逻辑。
_action$ = new Subject<Action>();
//........
dispatch(action: Action) {
this._action$.next(action);
}
这样,当我们把 Action 变成一个流以后,state 的变化就变得异常简单了。他们的关系就变成了,当 action$ emit value 的时候,state 会发生变化。不难写出以下代码
this._action$.pipe(
map(action => {
if (action === 'updateUser') {
// return new state.
}
if (action === 'updateUser') {
// return new state.
}
}),
).subscribe(this._state$);
不难看出来,其实,map 中的这段代码其实就是一个 reducer。
function reducer(state: State, action: Action) {
if (action === 'updateUser') {
// return new state.
}
if (action === 'updateUser') {
// return new state.
}
}
修改以下刚刚的代码,也就成了:
this._action$.pipe(scan(reducer, initialState))
我们再来看看原来的 class:
class Store {
state$: Observable<State>;
private _state$: BehaviorSubject<State>;
private _action$ = new Subject<Action>();
constructor(initialState: State, reducer: Reducer) {
this._state$ = new BehaviorSubject<State>(initialState);
this.state$ = this._state$.asObservable();
this._action$.pipe(scan(reducer, initialState)).subscribe(this._state$);
}
get state() {
return this._state$.value;
}
dispatch(action: Action) {
this._action$.next(action);
}
}
其实,这样我们已经实现了一个最简单的 Store,下面我们来看看如何实现一个简单的CRUD。
const reducer = function(state: State, action: Action) {
if (action.name === 'Add') {
return [
...state,
new User(),
];
}
if (action.name === 'Delete') {
const userId = action.payload.userId;
const userIndex = state.findIndex(user => user.id === userId);
return [
...state.slice(0, userIndex _ 1),
...state.slice(userIndex + 1),
];
}
if (action.name === 'Update') {
const user = action.payload.user;
const userIndex = state.findIndex(user => user.id === user.id);
return [
...state.slice(0, userIndex _ 1),
user,
...state.slice(userIndex + 1),
];
}
}
const userStore = new Store([], reducer);
当然,如果你去看 NgRx 的实现的话,会发现实际上,NgRx/Store 还是做了不少优化的,比如,Action 可以定义成一个 Object, Reducer 中的 if return 或者 switch 可以该用 on 实现,当然,还有对于异步,effects 的实现。