应用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约定,并非任何Best Practice。)
-
Observable
变量以$
结尾,如state$
; -
Subject
变量以$$
结尾, 如state$$
; -
Subscription
变量以_
结尾,如ultimate_
。
-
Observer、Observable、Subject、BehaviorSubject、Observable.prototype.scan运算符
,这几个上集已提过,不重复。 -
Obserable
与Subject
的资源消耗说明: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
。 -
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
。
= = = = = 分割线 = = = = =
主演:
-
action$$
、state$$
,这两位上集介绍过了,是两个Subject
,分别负责转推action
与state
。 -
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
。 -
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来启动
-
renderer
,还是老演员,上集没报姓名:在const ultimate_ = state$$.subscribe(console.log)
中,
这个console.log
就是上集的renderer
。本集的renderer
负责在state
更新时,更新dom
里的列表部分。 -
后端:JSON Server。
= = = = = 分割线 = = = = =
故事梗概:
-
triggers$
触发action$$.next(action)
,这个triggers$
可以是打包成Observable
的定时器、DOM事件或ajax等等; -
handlers
把action
map
成changeFn
,同时,更改dom
,可能会触发ajax observable
; -
changeFn
被scan
成state
; -
state
再被推给state$$
; -
state$$
再将state
转推给renderer
; -
renderer
更新dom
。 - 如果步骤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