用 RxJS 实现简单的 Redux

1,184 阅读4分钟

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 的实现。

其实我们发现,利用 RxJS 我们可以用比较少的代码实现一个简单的 State Management。而且,应该 Reducer 其实也并不是必须的,你甚至可以省略 reducer 的部分。在 Angular 中,利用 service 可以非常简单的实现这种读写抽离模式,防止数据在同一个地方莫名其妙的不知道在哪里修改了。

在 Angular 利用 service 抽离一个数据层是我个人比较推荐的做法,使用上也比较轻量级。(ngRx/Store 的引入是相对比较大的),最近 NgRx 也推出了一个轻量级的 state 方案,叫做,component/store, 有空也可以了解一下。