应用RxJS模拟Redux 第二集 Todo App - rxjs-space/notes GitHub Wiki

初稿日期:2016年11月08日

写作原因/目的

在上集Mock Redux with RxJS里,我们用RxJS模拟了Redux的最基本的功能,即实现了一个state为数字的state store。 现实生活中,state的内容要复杂的多,而且除了改变state,我们还要处理side effects,比如更新dom、发送ajax请求等等。

  • 本集想要说明的是:
    应用RxJS,在改变state的同时,我们可以很直接的处理side effects,不需要像Redux那样使用Thunk Middleware
    落实在代码上就是:
    // 从上集的
    const handlerA = (action: Action): ChangeFn => {return changeFn} // changeFn会被用来更改state
    // 变成本集的
    const handlerA = (action: Action): changeFn => {updateDom(); triggerAjax(); return changeFn}
    
    // handler所在位置(对应上集):
    action$$.map(action => {
      switch (action.type) {
        case A: return handlerA(action);
        ...
      }
    })
    仅此而已。
    接下来的部分都是上面代码的具体应用,大家赶时间的话,可以全部跳过。

本集承接上集,以Redux的思维,借助RxJS,制作一个Todo App。

变量名Style约定

(这是我个人的Style约定,并非任何Best Practice。)

  • Observable变量以$结尾,如state$
  • Subject变量以$$结尾, 如state$$
  • Subscription变量以_结尾,如ultimate_

相关概念

  1. Observer、Observable、Subject、BehaviorSubject、Observable.prototype.scan运算符,这几个上集已提过,不重复。

  2. ObserableSubject的资源消耗说明: Observable像是一个Function,每次运行sampleObservable$.subscribe(observer),都相当于一次sampleFunction.call(),是一次独立的运行,单独消耗资源。
    Subject维护一个observers列表,每次运行sampleSubject$$.subscribe(observer),只是运行了Array.prototype.push,将observer添加到observers列表,消耗资源很少。
    举例来说:

    const click$ = Observable.fromEvent(document, 'click');
    
    // 在文件A里:
    click$.subscribe(event => { // do some thing
    }); // 这相当于运行了一次addEventListener
    
    // 在文件B里:
    click$.subscribe(event => { // do some other thing
    }); // 又运行了一次addEventListener
    
    // 我们还可以这样:
    const click$$ = new Subject();
    const click$.subscribe(click$$); // 这里相当于运行一次addEventListener
    
    // 在文件A里:
    click$$.subscribe(event => { // do some thing
    }); // 这相当于向click$$的observers列表里添加了一个observer
    
    // 在文件B里:...

    所以,本集代码里的dom事件,90%以上都被打包成了Subject,用来节省资源。参见代码中的todo/dom-triggers/_shared.ts

  3. Observable.ajax运算符:将XMLHttpRequest打包成Observable,比如,POST一个新的Item到数据库:

    const post$ = Observable.ajax.post(urlAll, form);
    // 或者
    const post$ = Observable.ajax({
      url: urlAll,
      body: form,
      // headers: {'Content-Type': 'application/x-www-form-urlencoded'}, // 不用手动设置这个header
      method: 'POST'
    })
    // 然后,用subscribe启动这个XMLHttpRequest
    post$.subscribe(res => console.log(res), err => console.log(err));

编写代码

源码:代码在这个Repo里。
运行:npm install,然后npm run with-json-server

= = = = = 分割线 = = = = =

主演:

  1. action$$state$$,这两位上集介绍过了,是两个Subject,分别负责转推actionstate

  2. handlers,这位上集有出场,但是没报姓名,就在changeFn$map里:

    const changeFn$: Observable<ChangeFn> = action$$
      .map((action: Action) => {
        switch (action.type) {
          case INCREASE:
            return (state: State) => state + 1;
          // ...
        }
      })

    上面这个可以写成:const changeFn$ = action$$.map(handlers)
    上集中,handlers只根据action来返回一个changeFn
    本集中,handlers还要负责在更改dom(比如显示提示、使按钮失效等,不包括在dom上更新todo list)以及触发ajax observable

  3. triggers$,这位在上集叫做“外部”,就是触发action$$.next(action)的主体,可以是domEvent、ajax,但要打包成Observable,比如:

    const clickTrigger$ = Observable.fromEvent(document, 'click')
      .map(event => action$$.next({type: 'click', payload: event.XYZ}))
    // 每次点按,发送action
    // clickTrigger$不会自动运行,需要subscribe来启动
  4. renderer,还是老演员,上集没报姓名:在const ultimate_ = state$$.subscribe(console.log)中,
    这个console.log就是上集的renderer。本集的renderer负责在state更新时,更新dom里的列表部分。

  5. 后端:JSON Server

= = = = = 分割线 = = = = =

故事梗概:

  1. triggers$触发action$$.next(action),这个triggers$可以是打包成Observable的定时器、DOM事件或ajax等等;
  2. handlersaction mapchangeFn,同时,更改dom,可能会触发ajax observable
  3. changeFnscanstate
  4. state再被推给state$$
  5. state$$再将state转推给renderer;
  6. renderer更新dom
  7. 如果步骤2中触发了ajax observable,这个ajax observable就化身为triggers$,再从步骤1开始走一遍。

= = = = = 分割线 = = = = =
app启动为例,走一遍流程。

页面载入后,app启动,运行action$$.next({type: CONST.GET_ALL_START})
找找这个action的handler,

const changeFn$ = action$$.map(handlers);
export const handlers = (action: Action): ChangeFn => {
  switch (action.type) {
    case CONST.GET_ALL_START: return GET_ALL_START_handler(action);
    // ...
  }
}

就是这个GET_ALL_START_handler
就是这个GET_ALL_START_handler
就是这个GET_ALL_START_handler
(本文开篇说的就是它)

export const GET_ALL_START_handler = (action: Action): ChangeFn => {
  GET_ALL_START_handler_dom();
  GET_ALL_START_handler_ajax();
  return defaultChangeFnFac(action)
}

GET_ALL_START_handler_dom就是在dom上显示消息,通知用户:稍等一下,我读个数据。
GET_ALL_START_handler_ajax就是去读数据,要读所有数据,所以没参数。
defaultChangeFnFac(action)返回一个changeFn(跟handler_dom、handler_ajax没有任何关系),而后changeFn被scan成state,传给state$$,传给renderer。

回到GET_ALL_START_handler_ajax,具体是这样:

const GET_ALL_START_handler_ajax = () => {
  getAll$Fac().subscribe((response: any) => {
    const list: Item[] = response.response
    action$$.next({
      type: CONST.GET_ALL_COMPLETE,
      payload: {list}
    })
  }, (error) => {
    action$$.next({
      type: CONST.GET_ALL_FAIL,
      payload: {error}
    })
  })
}

getAll$Fac()返回一个Observable.ajax,subscribe启动这个Observable。
读取数据成功,触发action$$.next({type: complete}),失败就是action$$.next({type: fail}),再去找handler,转一圈。

= = = = = 分割线 = = = = =
来看一下index.ts,和上集区别不大:

import { handlers, domTriggers$, renderer } from ...

const stateInit: State = ...

export const action$$: Subject<Action> = new Subject()
const state$$: BehaviorSubject<State> = new BehaviorSubject(stateInit);

const ultimate_: Subscription = state$$.subscribe(renderer); // will log stateInit immediately
// 对应步骤5

const changeFn$: Observable<ChangeFn> = action$$
  .map(handlers)  // handlers将action map成changeFn,同时更改dom,触发ajax observable
// 对应步骤2

const state$: Observable<State> = changeFn$
  .scan((state, changeFn) => changeFn(state), state$$.getValue()) // 初始值是state$$.getValue(),即stateInit
// 对应步骤3

const intermediate_: Subscription = state$.subscribe(state$$) // state$开始向state$$推送
// 对应步骤4

const domTriggers_ = domTriggers$.subscribe(); // 启动domTriggers$
// 对应步骤1

action$$.next({type: CONST.GET_ALL_START}) // 这个action会触发ajax.getAll,从数据库读取已有的todo list;
// 对应步骤1

inputElem.focus(); // 可以输入新的todo了

总结

这样,是不是真的可以不用装redux了?

希望通过此文,能够帮助大家进一步了解RxJS。

参考:

Mock Redux with RxJS
Repo
Observable.ajax运算符
observers列表
JSON Server

⚠️ **GitHub.com Fallback** ⚠️