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
,所以異步的場景得藉助於社區形形色色的異步中間件,文中介紹了一些常見異步方案的使用,在實際項目中須要考慮多方面因素選擇適合本身團隊的異步方案。