本周精读的代码是 [react-rxjs](https://github.com/jarlah/react-rxjs)。

## 1 引言

本周精读的是 git 仓库 - react-rxjs，它给出了一个思路，让 rxjs 更好的与 react 结合。

## 2 概述

### View 层

View 层设计没商量，至少应该看不出 rxjs 的痕迹，它做到了：

```tsx
// view.tsx
export default (props) => (
    <div>
      {props.number}
      <button onClick={props.inc}>+</button>
      <button onClick={props.dec}>-</button>
    </div>
)
```

### Container 层

链接 View 与 Store 的层，同样也看不出 rxjs 的痕迹：

```tsx
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 对接起来：

```typescript
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` 函数：

```typescript
const inc$ = new Subject<void>()
export inc = () => inc$.next()
```

reducer 就是下面的 `reducer$`，整个 store 对应 `Observable.merge`，switch case 的地方被 `inc$`、`dec$` 自动识别出来了。

```typescript
const reducer$: Observable<(state: number) => number> = Observable.merge(
    inc$.map(() => (state: number) => state + 1),
    dec$.map(() => (state: number) => state - 1)
)
```

笔者优化一下代码结构，让 action 与 reducer 看起来更内聚：

```typescript
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 部分：

```typescript
const inc$ = new Subject<void>()
export inc = () => inc$.next()
```

可以看出，Action 功能很弱，我们只能触发 reducer，却无法 `mergeMap` 等流汇总的处理。

上周和叔叔讨论了 Rxjs 的一种代码组织方式：将 Rxjs 切成两部分使用，第一部分是数据源的抽象、聚合；第二部分是，对已经聚合过的单一数据源订阅后进行处理，这里处理过程只能包含对这个数据源的操作，不能再 merge 其他数据源。

这恰恰也是 Rxjs 在数据流中发挥的两大作用。分别是抽象，或者说是对副作用的隔离；以及强大的流处理能力。

react-rxjs 虽然代码看上去很简单，但 Action 部分没有足够的抽象能力，举例子说就是无法进行流的 merge，因为 `Subject` 自己就是一个事件触发器，想要进行流合并，必须发生在 reducer 中：

```typescript
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](https://github.com/redux-observable/redux-observable) 这个库，就很容易理解为什么这么做了。

```typescript
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 一摸一样：

```typescript
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](https://github.com/dvajs/dva) 的思路，减少 redux-observable 的样板代码。

## 4 总结

最后稍稍聊一下 cyclejs，因为用这个库，基本就脱离了 react 生态，我们 react 系开发者只能干瞪眼看看。

cyclejs 就一个目的，解决 react + rxjs 中阴魂不散的循环依赖问题：视图的回调函数可以产生数据源（observable），但视图又可能依赖这个数据源。

就是解决 A 依赖 B，B 又依赖 A 的问题，而且它做到了：

```typescript
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 更多讨论

> 讨论地址是：[精读《react-rxjs》 · Issue #65 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/65)

**如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，每周五发布。**
