精读《react-rxjs》
本周精读的代码是 react-rxjs。
1 引言
本周精读的是 git 仓库 - react-rxjs,它给出了一个思路,让 rxjs 更好的与 react 结合。
2 概述
View 层
View 层设计没商量,至少应该看不出 rxjs 的痕迹,它做到了:
// view.tsx
export default (props) => (
<div>
{props.number}
<button onClick={props.inc}>+</button>
<button onClick={props.dec}>-</button>
</div>
)
Container 层
链接 View 与 Store 的层,同样也看不出 rxjs 的痕迹:
import { inject } from 'react-rxjs'
import store$, { inc, dec } from './store'
import MyComponent from './view'
const props = (storeState: number): MyProps => ({
number: storeState,
inc,
dec
})
export default inject(store$, props)(MyComponent)
这里 storeState
就是 store 全部数据,注意 react-rxjs 是多 store 思想,所以 inject 第一个参数传入不同的 store,组件就会与对应的 store 绑定。
Store 层
这里代码就很有意思了,必须将 rxjs 与 action 对接起来:
import { createStore } from 'react-rxjs'
const inc$ = new Subject<void>()
const dec$ = new Subject<void>()
const reducer$: Observable<(state: number) => number> = Observable.merge(
inc$.map(() => (state: number) => state + 1),
dec$.map(() => (state: number) => state - 1)
)
const store$ = createStore("example", reducer$, 0)
export inc = () => inc$.next()
export dec = () => dec$.next()
export default store$
如果转换成 redux 思维,action 就是下面的 inc
函数:
const inc$ = new Subject<void>()
export inc = () => inc$.next()
reducer 就是下面的 reducer$
,整个 store 对应 Observable.merge
,switch case 的地方被 inc$
、dec$
自动识别出来了。
const reducer$: Observable<(state: number) => number> = Observable.merge(
inc$.map(() => (state: number) => state + 1),
dec$.map(() => (state: number) => state - 1)
)
笔者优化一下代码结构,让 action 与 reducer 看起来更内聚:
const inc$ = new Subject<void>()
export inc = () => inc$.next()
const incReducer = inc$.map(() => (state: number) => state + 1)
const dec$ = new Subject<void>()
export dec = () => dec$.next()
const decReducer = dec$.map(() => (state: number) => state - 1)
const reducer$: Observable<(state: number) => number> = Observable.merge(
incReducer,
decReducer
)
3 精读
让我们聚焦到 Action 部分:
const inc$ = new Subject<void>()
export inc = () => inc$.next()
可以看出,Action 功能很弱,我们只能触发 reducer,却无法 mergeMap
等流汇总的处理。
上周和叔叔讨论了 Rxjs 的一种代码组织方式:将 Rxjs 切成两部分使用,第一部分是数据源的抽象、聚合;第二部分是,对已经聚合过的单一数据源订阅后进行处理,这里处理过程只能包含对这个数据源的操作,不能再 merge 其他数据源。
这恰恰也是 Rxjs 在数据流中发挥的两大作用。分别是抽象,或者说是对副作用的隔离;以及强大的流处理能力。
react-rxjs 虽然代码看上去很简单,但 Action 部分没有足够的抽象能力,举例子说就是无法进行流的 merge,因为 Subject
自己就是一个事件触发器,想要进行流合并,必须发生在 reducer 中:
const incReducer = inc$.merge(requestUser$).map(() => (state: number) => state + 1)
但这样就丧失了 Action 与 Reducer 一一对应的关系,因为 reducer 可以擅自 merge 任意数据流,那就完全不受控制了。
所以回到第二个约定:对已经聚合过的单一数据源订阅后进行处理,此时不能包含任何 merge 操作。
可以总结一下,react-rxjs 的方式是解决了 rxjs 与 react 结合繁琐的问题,但如果遵守开发约定,Action 的功能就很弱,无法进行进一步抽象,如果不遵守开发约定,就可以解决 Action 能力弱的问题,但带来的是 Reducer 与 Action 脱离关系,这在项目维护中是不可接受的。
所以 react-rxjs 是一个看上去方便,但实践起来会发现怎么都不舒服的方案。
redux-observable
我们再看 redux-observable 这个库,就很容易理解为什么这么做了。
const pingEpic = action$ =>
action$.filter(action => action.type === 'PING')
.delay(1000) // Asynchronously wait 1000ms then continue
.mapTo({ type: 'PONG' });
// later...
dispatch({ type: 'PING' });
redux-observable 只有一个数据源,在 dispatch 的过程触发事件,进入 action 逻辑。其实每个 action 都源自对同一个数据源的订阅,通过 action.type
的筛选来确保执行了正确的 action。
所以每次 dispatch,包括 mapTo
也是 dispatch,都会触发数据源的事件派发,然后所有 Action 因为订阅了这个数据源,所以都会执行,最后被 .filter
逻辑拦截后,执行到正确的 Action。整个 Action 间调用的链路打个比方,就像我们使用微信一样,当触发任何消息,都会将其送到后台服务器,服务器给所有客户端发消息(假设系统设计的有问题,没有在服务端做 filter。。),每个客户端根据用户名做一个筛选,如果不是发给自己的消息,就过滤掉。然后,任何人与人之间的消息发送,都会走一遍这个流程。
reducer 与 redux 的 reducer 一摸一样:
const pingReducer = (state = { isPinging: false }, action) => {
switch (action.type) {
case 'PING':
return { isPinging: true };
case 'PONG':
return { isPinging: false };
default:
return state;
}
}
redux-observable 的设计比 react-rxjs 好在哪呢?我认为好在遵循了上面总结的两条经验:
第一部分是数据源的抽象、聚合;第二部分是,对已经聚合过的单一数据源订阅后进行处理,这里处理过程只能包含对这个数据源的操作,不能再 merge 其他数据源。
Action 之间的 dispatch 就是第一部分对数据源的整合,这里包括所有副作用。Reducer 只需要挑选合适的 ActionType 绑定,这样确保了 Reducer 中处理操作一定是对单一数据源的,不存在对其他数据源 merge,换句话说就是和 Action 一一对应。
所以整体来看,我认为 redux-observable 比 react-rxjs 要靠谱。
但是 react-rxjs 抛开了 redux 繁琐的样板代码,而 redux-observable 样板代码只会比 react-redux 要多。如果要投入项目使用,比较好的方式是按照 dva 的思路,减少 redux-observable 的样板代码。
4 总结
最后稍稍聊一下 cyclejs,因为用这个库,基本就脱离了 react 生态,我们 react 系开发者只能干瞪眼看看。
cyclejs 就一个目的,解决 react + rxjs 中阴魂不散的循环依赖问题:视图的回调函数可以产生数据源(observable),但视图又可能依赖这个数据源。
就是解决 A 依赖 B,B 又依赖 A 的问题,而且它做到了:
function main(sources) {
const input$ = sources.DOM.select('.field').events('input')
const name$ = input$.map(ev => ev.target.value).startWith('')
const vdom$ = name$.map(name =>
div([
label('Name:'),
input('.field', {attrs: {type: 'text'}}),
hr(),
h1('Hello ' + name),
])
)
return { DOM: vdom$ }
}
可以看到,最让我们不舒服的部分,就是 sources.DOM.select('.field')
和 input('.field')
这个循环节,为什么呢?因为初始化函数还没有返回 DOM 节点,为啥就能选中 DOM 节点?而且还作为参数参与这个 DOM 的生成。
可惜 React 无法解决这个问题,我们只能通过预定义数据源来解决:首先定义一个数据源,DOM 订阅它,Action 触发时找到这个数据源,手动调用 .next()
。或者 redux-observable 这样,全局只有一个数据源。
总的来说,笔者认为 rxjs 还是难以落地到 react 业务代码中,究其本质,就是没有 cyclejs 这种机制解决数据源引起的循环依赖问题。
5 更多讨论
如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。
发表评论 (审核通过后显示评论):