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 ,会造成无限循环:
// 不要这么做
const actionEpic = (action$) => action$; // 创建无限循环
这种处理副作用的方式和”过程管理“模式相似,有些地方也称为“saga“,但是 并不适用。如果你熟悉 redux-saga, redux-observable 和它很像。但是因为它使用了 RxJS, 所以它是更加声明式的,同时还可以扩展你现有的
RxJS 能力。
重要: redux-observable 并没有给
Observable.prototype
添加任何操作符,所以你需要在入口文件添加你使用的或者所有的操作符。
让我们从一个简单的 Epic 例子开始:
const pingEpic = action$ =>
action$.filter(action => action.type === 'PING')
.mapTo({ type: 'PONG' });
// 稍后...
dispatch({ type: 'PING' });
pingEpic
会监听类型为 PING
的 actions,然后投射为新的 action,PONG
。这个例子功能上相当于做了这件事情:
dispatch({ type: 'PING' });
dispatch({ type: 'PONG' });
牢记: Epics 运行在正常分发渠道旁, 在 reducers 完全接受到它们之后。当你将一个 action 投射成另一个 action, 你不会 阻止原始的 action 到达 reducers; 该 action 已经通过了它!
真正的力量来自于你需要做一些异步事情。假设我们需要在接受到 PING
1秒后分发 PONG
:
你的 reducers 会接收原始的 PING
action,然后 1 秒后接收 PONG
。
const pingReducer = (state = { isPinging: false }, action) => {
switch (action.type) {
case 'PING':
return { isPinging: true };
case 'PONG':
default:
return state;
}
};
因为过滤特定的 action 类型是很常见的需求,action$
流拥有 ofType()
操作符来减少这种复杂度。
const pingEpic = action$ =>
action$.ofType('PING')
.delay(1000) // 异步等待 1000ms 然后继续
.mapTo({ type: 'PONG' });
现在我们对 Epic 是什么有了大概的了解,让我们继续,看下这个更加真实的例子:
import { ajax } from 'rxjs/observable/dom/ajax';
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
// epic
const fetchUserEpic = action$ =>
action$.ofType(FETCH_USER)
.mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`)
.map(response => fetchUserFulfilled(response))
// 稍后...
dispatch(fetchUser('torvalds'));
我们使用
fetchUser
action 创建函数(工厂)代替直接创建 action POJO。这是一个完全可选的 Redux 惯例。
在 FETCH_USER_FULFILLED
action 的响应中,你可以修改你的 Store’s state。
你的 Epics 接收的第二个参数,一个轻量版的 Redux store。
type LightStore = { getState: Function, dispatch: Function };
function (action$: ActionsObservable<Action>, store: LightStore ): ActionsObservable<Action>;
这不是完整的 store 对象的引用,只包含了
store.getState()
和store.dispatch()
; 它Observable.from(store)
。
拥有它,你就可以调用 store.getState()
同步获取当前 state:
const INCREMENT = 'INCREMENT';
const INCREMENT_IF_ODD = 'INCREMENT_IF_ODD';
const increment = () => ({ type: INCREMENT });
const incrementIfOdd = () => ({ type: INCREMENT_IF_ODD });
const incrementIfOddEpic = (action$, store) =>
action$.ofType(INCREMENT_IF_ODD)
.filter(() => store.getState().counter % 2 === 1)
.map(() => increment());
// later...
dispatch(incrementIfOdd());
记住: 当 Epic 接收到 action, 它已经运行通过你的 reducers 并且 state 被修改了。
在 Epic 内部使用 store.dispatch()
是一个快速黑客的方便逃生舱,但是节制的使用。这被认为是反模式的并且会被在未来的版本中移除。
最后,redux-observable 提供了一个工具方法 ,该方法允许将多个 Epics 轻易的结合为一个:
import { combineEpics } from 'redux-observable';
const rootEpic = combineEpics(
pingEpic,
);
接下来,我们会探索怎样 激活 Epics 才能开始监听 actions。