有關 redux-saga 的文章,網絡上早已經是汗牛充棟。所以,本篇主要談一談本身的理解,以及實踐中的經驗總結。javascript
衆所周知,redux 大部分的想法,都來自於 elm。在 elm 和 redux 中,整個應用就是一個純函數。elm 經過在 reducer 中返回一些聲明反作用的 task 來處理異步問題,而 redux 借鑑 koa 的插件機制,用中間件改造 dispatch ,從而誕生了一批經過構造知足特殊 pattern 條件的 action 來解決反作用的問題。html
而 redux-saga 獨闢蹊徑,監聽 action 來執行有反作用的 task,以保持 action 的簡潔性。而且引入了 sagas 的機制和 generator 的特性,讓redux-saga 很是方便地處理複雜異步問題。前端
有意思的是,redux 借鑑了 elm,但在處理異步問題(反作用問題在前端通常爲異步問題)上,借鑑了 koa 中間件的形式,而 redux-saga 卻又去從 elm 取經,借鑑了獨立 task 的形式。可是說到底,redux-saga 是一個 redux 的中間件。這個故事告訴咱們,有好的設計不若有強大的擴展性。java
redux-saga 自己也有良好的擴展性。好比,易證得,但凡 redux 中間件,均可以用 redux-saga 來重寫。固然了,不是說用了 redux-saga,其它異步中間件就不能用了,只是說不能保證 redux-saga 能剛好和你以前使用的中間件配合良好。git
redux-saga
簡介redux-saga 是一個 redux 中間件,它具備以下特性:github
集中處理 redux 反作用問題。json
被實現爲 generator 。redux
類 redux-thunk 中間件。api
watch/worker(監聽->執行) 的工做形式。promise
讀者也能夠從這裏查看官方定義。
對於剛接觸 redux-saga 的同窗,能夠先來一段簡單的代碼快速瞭解 redux-saga 諸多特性。
// 類 thunk 的 worker 「進程」 function* load() { yield put({ type: BEGIN_LOAD_DATA }); try { const result = yield call(fetch, UrlMap.loadData); yield put({ type: LOAD_DATA_SUCCESS, payload: result, }); } catch (e) { yield put({ type: LOAD_DATA_ERROR, payload: e, error: true, }); } } function* saga() { // 建立一個監聽「進程」 yield fork(watch(CLICK_LOAD_BUTTON, load)) }
Effect 是一個 javascript 對象,裏面包含描述反作用的信息,能夠經過 yield 傳達給 sagaMiddleware 執行
在 redux-saga 世界裏,全部的 Effect 都必須被 yield 纔會執行,因此有人寫了 eslint-plugin-redux-saga 來檢查是否每一個 Effect 都被 yield。而且原則上來講,全部的 yield 後面也只能跟Effect,以保證代碼的易測性。
例如:
yield fetch(UrlMap.fetchData);
應該用 call Effect :
yield call(fetch, UrlMap.fetchData)
從而可使代碼可測:
assert.deepEqual(iterator.next().value, call(fetch, UrlMap.fetchData))
關於各個 Effect 的具體介紹,文檔已經寫得很詳細了,這裏只作簡要介紹。
做用和 redux 中的 dispatch 相同。
yield put({ type: 'CLICK_BTN' });
做用和 redux thunk 中的 getState 相同。
const id = yield select(state => state.id);
等待 redux dispatch 匹配某個 pattern 的 action 。
在這個例子中,先等待一個按鈕點擊的 action ,而後執行按鈕點擊的 saga:
while (true) { yield take('CLICK_BUTTON'); yield fork(clickButtonSaga); }
再舉一個利用 take 實現 logMiddleware 的例子:
while (true) { const action = yield take('*'); const newState = yield select(); console.log('received action:', action); console.log('state become:', newState); }
這種監聽一個 action ,而後執行相應任務的方式,在 redux-saga 中很是經常使用,所以 redux-saga 提供了一個輔助 Effect —— takeEvery ,讓 watch/worker 的代碼更加清晰。
yield takeEvery('*', function* logger(action) { const newState = yield select(); console.log('received action:', action); console.log('state become:', newState); });
redux-saga 能夠用 fork 和 call 來調用子 saga ,其中 fork 是無阻塞型調用,call 是阻塞型調用。
若是看過 saga 的論文,就知道 saga 是由許多子 saga (或者 subtransaction)組合起來的。fork Effect 和它的字面意思同樣,即建立一個子 saga 。
下面寫一個倒數的例子,當接收到 BEGIN_COUNT 的 action,則開始倒數,而接收到 STOP_COUNT 的 action, 則中止倒數。
function* count(number) { let currNum = number; while (currNum >= 0) { console.log(currNum--); yield delay(1000); } } function countSaga* () { while (true) { const { payload: number } = yield take(BEGIN_COUNT); const countTaskId = yield fork(count, number); yield take(STOP_TASK); yield cancel(countTaskId); } }
有阻塞地調用 saga 或者返回 promise 的函數。
一樣寫一個例子:
const project = yield call(fetch, { url: UrlMap.fetchProject }); const members = yield call(fetchMembers, project.id);
在介紹 redux-saga 優缺點以前,這裏先簡要介紹傳統的 redux 異步中間件,以便和 redux-saga 作比較。對傳統異步中間件已經充分了解的讀者,能夠直接跳到 「redux-saga 優缺點分析」 進行閱讀。
使用redux的前端技術團隊或我的,大多數都有一套本身 fetch-middleware,一來能夠封裝異步請求的業務邏輯,避免重複代碼,二來能夠寫一些公共的異步請求邏輯,好比異常接口數據採集、接口緩存、接口處理等等。例如 redux-composable-fetch,redux-api-middleware。
在當前 redux 社區中,fetch-middleware 封裝結果通常以下:
function loadData(id) { return { url: '/api.json', types: [LOADING_ACTION_TYPE, SUCCESS_ACTION_TYPE, SUCCESS_ACTION_TYPE], params: { id, }, }; }
值得一提的是,大多數 fetch-middleware 都會用到一個小技巧 —— 把最終處理好的 promise 返回出來,以便在 thunk-middleware 中複用,並組織不一樣異步過程的前後邏輯。
function loadDetailThunk(id) { return (dispatch) => { // 先請求到 loadData 的結果,再請求 loadDetail dispatch(loadData(id)).then(result => { const { id: detailId } = result; dispatch(loadDetail(detailId)); }); }; }
這個技巧在 redux-saga
中也一樣有效。
function* loadDetailSaga(id) { const result = yield put.sync(loadData(id)); const { id: detailId } = result; yield put.sync(loadDetail(detailId)); }
redux 中大量應用了 thunk 的概念,例如 getState 以延遲執行的方式能夠始終得到最新值,redux-thunk 以延遲執行的方式把反作用的責任推卸到用戶身上。
任何異步問題都能在 thunk 中解決。
sequence-middleware 用於保證 action 依次執行,不管是異步 action 仍是普通 aciton ,和 fetch-middleware 配合使用很是方便。
這裏能夠把每一個 action 能夠寫成 thunk action,在 thunk 函數內從 store 拿到參數,避免 action 之間的依賴。這樣無論業務邏輯有多複雜,均可以經過用 sequence action 輕易組織。
function loadDetailThunk() { return function(dispatch, getState) { const detailId = _.get(getState(), `${currPath}.detailId`); dispatch({ url: UrlMap.getDetail, params: { detailId }, }); }; } function loadDetail() { return [loadData(), loadDetailThunk()]; }
redux-saga
優缺點分析redux-saga 不強迫咱們捕獲異常,這每每會形成異常發生時難以發現緣由。所以,一個良好的習慣是,相信任何一個過程都有可能發生異常。若是出現異常但沒有被捕獲,redux-saga 的錯誤棧會給你一種一臉懵逼的感受。
generator 的調試環境比較糟糕,babel 的 source-map 常常錯位,常常要手動加 debugger 來調試。
你團隊中使用的其它異步中間件,或許難以和 redux-saga 搭配良好。或許須要花費一些代價,用 redux-saga 來重構一部分中間件。
保持 action 的簡單純粹,aciton 再也不像原來那樣五花八門,讓人眼花繚亂。task 的模式使代碼更加清晰。
redux-saga 提供了豐富的 Effects,以及 sagas 的機制(全部的 saga 均可以被中斷),在處理複雜的異步問題上十分趁手。若是你的應用屬於寫操做密集型或者業務邏輯複雜,快讓 redux-saga 來拯救你。
擴展性強。
聲明式的 Effects,使代碼更易測試,查看詳情。
用 redux-saga 來寫中間件,可謂事半功倍。這裏舉一個輪詢中間件的例子。
function* pollingSaga(fetchAction) { const { defaultInterval, mockInterval } = fetchAction; while (true) { try { const result = yield put.sync(fetchAction); const interval = mockInterval || result.interval; yield delay(interval * 1000); } catch (e) { yield delay(defaultInterval * 1000); } } } function* beginPolling(pollingAction) { const { pollingUrl, defaultInterval = 300, mockInterval, types, params = {} } = pollingAction; if (!types[1]) { console.error('pollingAction pattern error', pollingAction); throw Error('pollingAction types[1] is null'); } const fetchAction = { url: pollingUrl, types, params, mockInterval, defaultInterval, }; const pollingTaskId = yield fork(pollingSaga, fetchAction); const pattern = action => action.type === types[1] && action.stopPolling; yield take(pattern); yield cancel(pollingTaskId); } function* pollingSagaMiddleware() { yield takeEvery(action => { const { pollingUrl, types } = action; return pollingUrl && types && types.length; }, beginPolling); };
最後,redux-saga
在實踐的沉澱,我已經總結到 redux-saga-sugar,歡迎點贊 ~