Redux 的核心理念是严格的单向数据流,只能经过 dispatch(action)
的方式修改 store,流程以下:javascript
view -> action -> reducer -> store复制代码
而在实际业务中每每有大量异步场景,最原始的作法是在 React 组件 componentDidMount
的时候初始化异步流,经过 callback
或者 promise
的方式在调用 dispatch(action)
,这样作把 view
层和 model
层混杂在一块儿,耦合严重,后期维护很是困难。
以前的文章 解读 Redux 中间件的原理 能够知道,中间件(middleware)改写了 dispatch
方法,所以能够更灵活的控制 dispatch
的时机,这对于处理异步场景很是有效。所以 Redux 做者也建议用中间件来处理异步流。社区常见的中间件有 redux-thunk
、redux-promise
、redux-saga
、redux-observable
等。java
做为 Redux 做者本身写的异步中间件,其原理很是简单:Redux 自己只会处理同步的简单对象 action,但能够经过 redux-thunk
拦截处理函数(function)类型的 action
,经过回调来控制触发普通 action
,从而达到异步的目的。其典型用法以下:react
//constants 部分省略
//action creator
const createFetchDataAction = function(id) {
return function(dispatch, getState) {
dispatch({
type: FETCH_DATA_START,
payload: id
})
api.fetchData(id)
.then(response => {
dispatch({
type: FETCH_DATA_SUCCESS,
payload: response
})
})
.catch(error => {
dispatch({
type: FETCH_DATA_FAILED,
payload: error
})
})
}
}
//reducer
const reducer = function(oldState, action) {
switch(action.type) {
case FETCH_DATA_START :
// 处理 loading 等
case FETCH_DATA_SUCCESS :
// 更新 store 等处理
case FETCH_DATA_FAILED :
// 提示异常
}
}复制代码
能够看到采用 redux-thunk
后,action creator
返回的 action 能够是个 function,这个 function 内部本身会在合适的时机 dispatch
合适的普通 action。而这里面也没有什么魔法,redux-thunk
其核心源码以下:web
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};复制代码
若是 action 是个 function,便将 dispatch
方法传入该函数并执行之。redux-thunk
在使用时很是方便,能知足大部分场景,缺点就是样板代码太多,写起来费劲了点。编程
redux-thunk
是将从 api 返回的 promise resolve 后 dispatch
成不一样 action,那直接将这个 promise 做为 action 给 dispatch
,让中间件来处理 resolve 这个过程,岂不是就能够少写些 .then().catch()
之类的代码了吗?redux-promise
正是解决了这个问题。一样是从后端去数据,其典型用法为:json
const FETCH_DATA = 'FETCH_DATA'
//action creator
const getData = function(id) {
return {
type: FETCH_DATA,
payload: api.fetchData(id) // 直接将 promise 做为 payload
}
}
//reducer
const reducer = function(oldState, action) {
switch(action.type) {
case FETCH_DATA:
if (action.status === 'success') {
// 更新 store 等处理
} else {
// 提示异常
}
}
}复制代码
这样下来比 redux-thunk
的写法瘦身很多。其核心源码与 redux-thunk
相似,若是 action
或 action.payload
是 Promise
类型则将其 resolve,触发当前 action
的拷贝,并将 payload 设置为 promise 的 成功/失败结果。redux
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {// 判断是不是标准的 flux action
return isPromise(action)
? action.then(dispatch)
: next(action);
}
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
};
}复制代码
仔细一看会发现 redux-promise
的写法里 reducer
收到 action 时就已经被 resolve 了,这样若是要处理 loading
这种情景就还得写额外代码,并且在 action 这样一个简单对象里增长 status
属性会给人不规范的感受,这可能就是步子迈大了容易扯到蛋吧。后端
redux-thunk
和 redux-promise
用法实际上比较相似,都是触发一个 function/promise 让中间件本身决定 dispatch
真正异步数据的时机,这对于大部分场景来讲已经足够了。可是对于异步状况更复杂的场景,咱们每每要写不少业务代码,一个异步结果返回后可能须要对应修改 store
里多个部分,这样就面临一个困惑的问题:业务代码是放在 action
层仍是 reducer
里?例如,管理员冻结某用户的帐户,须要同时更新 store
里 AllUserList
和 PendingUserlist
, 这时候面临两种选择 :api
PEND_USER
的 action,而后在 reducer 对应 switch 里同时更新 AllUserList
和 PendingUserlist
REFRESH_USER_LIST
和 REFRESH_PENDING_USER_LIST
两个 action,而后在 reducer 里分别更新两处 store
。REFRESH_USER_LIST
的地方,将 action 拆的更新更利于复用,这时候就得作个取舍了。而 redux-saga
就能够很好的解决这个问题,它在原来 Redux 数据流中增长了 saga
层(不要在乎这个诡异的名字😂),监听 action 并衍生出新的 action 来对 store 进行操做,这一点接下来介绍的 redux-observable
同样,核心用法能够总结为: Acion in,action out
。promise
用对于刚才的问题,redux-saga
的写法为:
//action creator
const refreshUserListAction = (id)=>({type:REFRESH_USER_LIST,id:pendedUser.id})
const refreshPendingUserListAction = (id)=>({type:REFRESH_PENGDING_USER_LIST,id:pendedUser.id})
//saga
function* refreshLists() {
const pendedUser = yield call(api.pendUser)
// 将同时触发(put)两个 action
yield put(refreshUserListAction())
yield put(refreshPendingUserListAction())
}
function* watchPendUser() {
while ( yield take(PEND_USER) ) {
yield call(refreshLists) // 监听 PEND_USER 的 action,并执行(call)refreshLists 方法
}
}
//reducer 省略复制代码
这样一来业务逻辑就很是明确了:由一个'PEND_USER'触发了两个 REFRESH
的 action 并进入 reducer
。并且将业务代码分离出 action
层和 reducer
层,减小了代码耦合,对于后期维护和测试很是有益。
对于更复杂的异步,例如竞态问题,redux-saga
更能大显身手了:
以前用过一个第三方的微博客户端,发现的一个 bug:当点击第一条微博 A,跳转到 A 的评论页,因为网速缘由 loading 过久不肯意再等了,就返回主页,再点了另外一条微博 B,跳转到 B 的评论页,这时候先前的 A 的评论列表请求返回了,因而在 B 微博的评论页里展现了 A 的评论。
若是这个系统是用 react/redux 作的话,那这个 bug 的缘由很明显:action
在到达 reducer
的时候该 action
已经不须要了。若是用 redux-thunk
/redux-promise
来解决此问题的话有两种方式:
id
和 promise 开始前的 id
是否相同:function fetchWeiboComment(id){
return (dispatch, getState) => {
dispatch({type: 'FETCH_COMMENT_START', payload: id});
dispatch({type: 'SET_CURRENT_WEIBO', payload: id}); // 设置 store 里 currentWeibo 字段
return api.getComment(id)
.then(response => response.json())
.then(json => {
const { currentWeibo } = getState(); // 判断当前 store 里的 id 和 promise 开始前的 id 是否相同:
(currentFriend === id) && dispatch({type: 'FETCH_COMMENT_DONE', playload: json})
});
}
}复制代码
id
,在 reducer 处理的时候判断这个 id
和 url
里的 id
是否相同, 这里就不上代码了。总之这样处理会比较多的代码,若是项目中有大量这种场景,最后维护起来会比较蛋疼。而用 redux-saga 能够处理以下:
import { takeLatest } from `redux-saga`
function* fetchComment(action) {
const comment = yield call(api.getComment(action.payload.id))
dispatch({type: 'FETCH_COMMENT_DONE', payload: comment})
}
function* watchLastFetchWeiboComment() {
yield takeLatest('FETCH_COMMENT_START', fetchComment)
}复制代码
takeLatest
方法能够过滤 action
,当下一个 FETCH_COMMENT_START
的 action
到来时取消上一个 FETCH_COMMENT_START
的 action
的触发,这时候未返回结果的上一条网络请求(pending 状态)会被 cancel 掉。
另外 redux-saga
还提供了更多方法用来处理异步请求的阻塞、并发等场景,更多操做能够看 Redux-saga 中文文档 。
所以若是项目中有大量复杂异步场景,就很是适合采用 redux-saga。
采用 redux-saga
能够保持 action
和 reducer
的简单可读,逻辑清晰,经过采用 Generator
,能够很方便地处理不少异步状况,而 redux-saga
的缺点就是会新增一层 saga
层,增大上手难度;Generator
函数代码调试也比普通函数更复杂。
能够看到 redux-saga
的思路和以前的 redux-thunk
有很大不一样,它是响应式的(Reactive Programming):
在计算机中,响应式编程是一种面向数据流和变化传播的编程范式。这意味着能够在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值经过数据流进行传播。
对于数据流的起点 action 层来讲,只须要触发 FETCH_COMMENT_START
的事件流即可完成整个数据的更新,无需关心后续数据的变化处理。
提及响应式,就不得不提 RxJS
了,RxJS
是一个强大的 Reactive 编程库,提供了强大的数据流组合与控制能力。RxJS
中 “一切皆流” 的思想对于接触函数式编程(FP)很少的用户来讲会感到很是困惑,但在熟练了以后又会豁然开朗。在 RxJS
中,一个观察者 (Observer
) 订阅一个可观察对象 (Observable
),下面是 Observable 和传统 Promise
、Generator
的对比:
能够看到 Observable
能够 异步 地返回 多个 结果,所以有着更强大的数据的操做控制能力。而 redux-observable
即是基于 RxJS
实现的经过组合和取消异步动做去建立反作用的中间件。redux-observable
中处理异步的这一层叫 Epic
(也不要在乎这个诡异的名字),Epic 接收一个以 action 流为参数的函数,并返回一个 action 流。
先来看看简单的例子:
//epic
const fetchWeiboCommentEpic = action$=>
action$.ofType(FETCH_COMMENT_START) //ofType 表示过滤type 为 FETCH_COMMENT_START 的 action
.switchMap(action=>//switchMap 的做用相似 saga 中的 takeLatest,新的 action 会将老的 action 取消掉
Observable.fromPromise(api.getComment(action.payload.id))// 将 promise 转化成 Observable
.map(comment=>({type: 'FETCH_COMMENT_DONE', payload: comment})) // 将返回的 Obsevable 映射(map)成一个普通 action
.catch(err=>({type: 'FETCH_COMMENT_ERROR', payload: err})) // 这里的 err 也是一个 Observable,被捕获并映射成了一个 action
)复制代码
配置好 redux-observable
中间件后便可监听 FETCH_COMMENT_START
的 action
并异步发起请求并返回携带相应数据的成功或失败的 action
。能够看到,得益于 RxJS
强大的诸如 switchMap
的操做符,redux-observable
能用简短的代码完成复杂的数据控制过程。咱们还能够在这个 fetchWeiboCommentEpic
中增长更复杂的操做,好比当收到 FETCH_COMMENT_START
时延迟 500ms 再发请求,并收到人为取消的 actionFETCH_COMMENT_FORCE_STOP
时(好比用户点了取消加载的按钮)终止请求,拿到微博评论后同时提醒 “刷新成功”:
//epic
const fetchWeiboCommentEpic = action$=>
action$.ofType(FETCH_COMMENT_START)
.delay(500) // 延迟 500ms 再启动
.switchMap(action=>
Observable.fromPromise(api.getComment(action.payload.id))
.map(comment=>[
{type: 'FETCH_COMMENT_DONE', payload: comment},
{type: 'SET_NOTIFICATION', payload: comment} // 同时提醒 “刷新成功”
])
.catch(err=>({type: 'FETCH_COMMENT_ERROR', payload: err}))
.takeUntil(action$.ofType('FETCH_COMMENT_FORCE_STOP')) // 人为取消加载
)复制代码
再来看个场景,用户在搜索框打字时,实时从后端取结果返回最匹配的提示(相似在 Google 搜索时展现的提示)。用户打字不停地触发 USER_TYPING
的 action,不停去请求后端,这种时候用 redux-thunk
处理就会比较麻烦,而 redux-observable
能够优雅地作到:
const replaceUrl=(query)=>({type:'REPLACE_URL',payload:query})
const receiveResults = results=>({type:'SHOW_RESULTS',payload:results})
const searchEpic = action$=>action$.ofType('USER_TYPING')
.debounce(500) // 这里作了 500ms 的防抖,500ms 内不停的触发打字的操做将不会发起请求,这样大大节约了性能
.map(action => action.payload.query) // 返回 action 里的 query 字段,接下来的函数收到参数即是 query 而不是 action 整个对象了
.filter(query => !!query) // 过滤掉 query 为空的状况
.switchMap(query =>
.takeUntil(action$.ofType('CLEARED_SEARCH_RESULTS'))
.mergeMap(() => Observable.merge( // 将两个 action 以 Observable 的形式 merge 起来
Observable.of(replaceUrl(`?q=${query}`)),
Observable.fromPromise(api.search(query))
.map(receiveResults)
))
);复制代码
另外 RxJS
还提供了 WebSocketSubject
对象,能够很容易优雅地处理 websocket
等场景,这里就不展开了。redux-observable
提供的 Observable
比 Generator
更灵活,得益于强大的 RxJS
,redux-observable
对异步的处理能力更为强大,这大概是目前最优雅的 redux
异步解决方案了。然而缺点也很明显,就是上手难度过高,光是 RxJS
的基本概念对于不熟悉响应式编程的同窗来讲就不是那么好啃的。可是经过此来接触 RxJS
的思想,能开阔本身眼界,也是很是值得的。所以在异步场景比较复杂的小项目中能够尝试使用 redux-observable
,而大型多人协做的项目中得考虑整个团队学习的成本了,这种状况通常用 redux-saga
的性价比会更高。目前国内采用 redux-observable
的并很少,在这里也但愿能够和你们多交流下 redux-observable
相关的实践经验。
Redux
自己只会处理同步的 action
,所以异步的场景得借助于社区形形色色的异步中间件,文中介绍了一些常见异步方案的使用,在实际项目中须要考虑多方面因素选择适合本身团队的异步方案。