Epics

Epic 是 redux-observable 的核心原语。

它是一个函数,接收 actions 流作为参数并且返回 actions 流。 Actions 入, actions 出.

它的签名如下:

虽然通常你会响应接收到的 action 而产出 actions,但这不是必须的!一旦进入你的 Epic,使用任何你想使用的 Observable 模式,只要最后返回 action 流即可。

你发出的 actions 会通过 立刻被分发,所以 redux-observable 实际上会做 epic(action$, store).subscribe(store.dispatch)

Epics 运行在正常的 Redux 分发通道旁,在 reducers 接受到之后,所以不会 “吞掉” 一个 action。
在 Epics 实际接收 Actions 前,Actions 将始终贯穿你的 reducers。

如果你传出 传入的 action ,会造成无限循环:

  1. // 不要这么做
  2. const actionEpic = (action$) => action$; // 创建无限循环

这种处理副作用的方式和”过程管理“模式相似,有些地方也称为saga,但是 并不适用。如果你熟悉 redux-saga, redux-observable 和它很像。但是因为它使用了 RxJS, 所以它是更加声明式的,同时还可以扩展你现有的
RxJS 能力。

重要: redux-observable 并没有给 Observable.prototype 添加任何操作符,所以你需要在入口文件添加你使用的或者所有的操作符。

让我们从一个简单的 Epic 例子开始:

  1. const pingEpic = action$ =>
  2. action$.filter(action => action.type === 'PING')
  3. .mapTo({ type: 'PONG' });
  4. // 稍后...
  5. dispatch({ type: 'PING' });

pingEpic 会监听类型为 PING 的 actions,然后投射为新的 action,PONG。这个例子功能上相当于做了这件事情:

  1. dispatch({ type: 'PING' });
  2. dispatch({ type: 'PONG' });

牢记: Epics 运行在正常分发渠道旁, 在 reducers 完全接受到它们之后。当你将一个 action 投射成另一个 action, 你不会 阻止原始的 action 到达 reducers; 该 action 已经通过了它!

真正的力量来自于你需要做一些异步事情。假设我们需要在接受到 PING 1秒后分发 PONG:

你的 reducers 会接收原始的 PING action,然后 1 秒后接收 PONG

  1. const pingReducer = (state = { isPinging: false }, action) => {
  2. switch (action.type) {
  3. case 'PING':
  4. return { isPinging: true };
  5. case 'PONG':
  6. default:
  7. return state;
  8. }
  9. };

因为过滤特定的 action 类型是很常见的需求,action$ 流拥有 ofType() 操作符来减少这种复杂度。

  1. const pingEpic = action$ =>
  2. action$.ofType('PING')
  3. .delay(1000) // 异步等待 1000ms 然后继续
  4. .mapTo({ type: 'PONG' });

现在我们对 Epic 是什么有了大概的了解,让我们继续,看下这个更加真实的例子:

  1. import { ajax } from 'rxjs/observable/dom/ajax';
  2. // action creators
  3. const fetchUser = username => ({ type: FETCH_USER, payload: username });
  4. const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
  5. // epic
  6. const fetchUserEpic = action$ =>
  7. action$.ofType(FETCH_USER)
  8. .mergeMap(action =>
  9. ajax.getJSON(`https://api.github.com/users/${action.payload}`)
  10. .map(response => fetchUserFulfilled(response))
  11. // 稍后...
  12. dispatch(fetchUser('torvalds'));

我们使用 fetchUser action 创建函数(工厂)代替直接创建 action POJO。这是一个完全可选的 Redux 惯例。

FETCH_USER_FULFILLED action 的响应中,你可以修改你的 Store’s state。


View this demo on JSBin

你的 Epics 接收的第二个参数,一个轻量版的 Redux store。

  1. type LightStore = { getState: Function, dispatch: Function };
  2. function (action$: ActionsObservable<Action>, store: LightStore ): ActionsObservable<Action>;

这不是完整的 store 对象的引用,只包含了 store.getState()store.dispatch(); 它Observable.from(store)

拥有它,你就可以调用 store.getState() 同步获取当前 state:

  1. const INCREMENT = 'INCREMENT';
  2. const INCREMENT_IF_ODD = 'INCREMENT_IF_ODD';
  3. const increment = () => ({ type: INCREMENT });
  4. const incrementIfOdd = () => ({ type: INCREMENT_IF_ODD });
  5. const incrementIfOddEpic = (action$, store) =>
  6. action$.ofType(INCREMENT_IF_ODD)
  7. .filter(() => store.getState().counter % 2 === 1)
  8. .map(() => increment());
  9. // later...
  10. dispatch(incrementIfOdd());

记住: 当 Epic 接收到 action, 它已经运行通过你的 reducers 并且 state 被修改了。

在 Epic 内部使用 store.dispatch() 是一个快速黑客的方便逃生舱,但是节制的使用。这被认为是反模式的并且会被在未来的版本中移除。


View this demo on JSBinn

最后,redux-observable 提供了一个工具方法 ,该方法允许将多个 Epics 轻易的结合为一个:

  1. import { combineEpics } from 'redux-observable';
  2. const rootEpic = combineEpics(
  3. pingEpic,
  4. );

接下来,我们会探索怎样 激活 Epics 才能开始监听 actions。