redux-saga 实践总结
有關(guān) redux-saga 的文章,網(wǎng)絡(luò)上早已是汗牛充棟。因此,本篇主要談一談自己的理解,以及實(shí)踐中的經(jīng)驗(yàn)總結(jié)。
眾所周知,redux 大部分的想法,都來自于?elm。在 elm 和 redux 中,整個(gè)應(yīng)用就是一個(gè)純函數(shù)。elm 通過在 reducer 中返回一些聲明副作用的 task 來處理異步問題,而 redux 借鑒 koa 的插件機(jī)制,用中間件改造 dispatch ,從而誕生了一批通過構(gòu)造滿足特殊 pattern 條件的 action 來解決副作用的問題。
而 redux-saga 獨(dú)辟蹊徑,監(jiān)聽 action 來執(zhí)行有副作用的 task,以保持 action 的簡(jiǎn)潔性。并且引入了?sagas?的機(jī)制和 generator 的特性,讓redux-saga 非常方便地處理復(fù)雜異步問題。
有意思的是,redux 借鑒了 elm,但在處理異步問題(副作用問題在前端一般為異步問題)上,借鑒了 koa 中間件的形式,而 redux-saga 卻又去從 elm 取經(jīng),借鑒了獨(dú)立 task 的形式。但是說到底,redux-saga 是一個(gè) redux 的中間件。這個(gè)故事告訴我們,有好的設(shè)計(jì)不如有強(qiáng)大的擴(kuò)展性。
redux-saga 本身也有良好的擴(kuò)展性。比如,易證得,但凡 redux 中間件,都可以用 redux-saga 來重寫。當(dāng)然了,不是說用了 redux-saga,其它異步中間件就不能用了,只是說不能保證 redux-saga 能恰好和你之前使用的中間件配合良好。
redux-saga?簡(jiǎn)介
redux-saga 是一個(gè) redux 中間件,它具有如下特性:
-
集中處理 redux 副作用問題。
-
被實(shí)現(xiàn)為 generator 。
-
類 redux-thunk 中間件。
-
watch/worker(監(jiān)聽->執(zhí)行) 的工作形式。
讀者也可以從這里查看官方定義。
對(duì)于剛接觸 redux-saga 的同學(xué),可以先來一段簡(jiǎn)單的代碼快速了解 redux-saga 諸多特性。
// 類 thunk 的 worker “進(jìn)程” 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() {// 創(chuàng)建一個(gè)監(jiān)聽“進(jìn)程”yield fork(watch(CLICK_LOAD_BUTTON, load)) }Effects
Effect 是一個(gè) javascript 對(duì)象,里面包含描述副作用的信息,可以通過 yield 傳達(dá)給 sagaMiddleware 執(zhí)行
在 redux-saga 世界里,所有的 Effect 都必須被 yield 才會(huì)執(zhí)行,所以有人寫了?eslint-plugin-redux-saga?來檢查是否每個(gè) Effect 都被 yield。并且原則上來說,所有的 yield 后面也只能跟Effect,以保證代碼的易測(cè)性。
例如:
yield?fetch(UrlMap.fetchData);應(yīng)該用 call Effect :
yield?call(fetch, UrlMap.fetchData)從而可以使代碼可測(cè):
assert.deepEqual(iterator.next().value, call(fetch, UrlMap.fetchData))關(guān)于各個(gè) Effect 的具體介紹,文檔已經(jīng)寫得很詳細(xì)了,這里只做簡(jiǎn)要介紹。
1、put
作用和 redux 中的 dispatch 相同。
yield?put({?type:?'CLICK_BTN'?});2、select
作用和 redux thunk 中的 getState 相同。
const?id =?yield?select(state?=>?state.id);3、take
等待 redux dispatch 匹配某個(gè) pattern 的 action 。
在這個(gè)例子中,先等待一個(gè)按鈕點(diǎn)擊的 action ,然后執(zhí)行按鈕點(diǎn)擊的 saga:
while (true) {yield take('CLICK_BUTTON');yield fork(clickButtonSaga); }再舉一個(gè)利用 take 實(shí)現(xiàn) logMiddleware 的例子:
while (true) {const action = yield take('*');const newState = yield select();console.log('received action:', action);console.log('state become:', newState); }這種監(jiān)聽一個(gè) action ,然后執(zhí)行相應(yīng)任務(wù)的方式,在 redux-saga 中非常常用,因此 redux-saga 提供了一個(gè)輔助 Effect —— takeEvery ,讓 watch/worker 的代碼更加清晰。
yield takeEvery('*', function* logger(action) {const newState = yield select();console.log('received action:', action);console.log('state become:', newState); });4、阻塞調(diào)用和無阻塞調(diào)用
redux-saga 可以用 fork 和 call 來調(diào)用子 saga ,其中 fork 是無阻塞型調(diào)用,call 是阻塞型調(diào)用。
如果看過 saga 的論文,就知道 saga 是由許多子 saga (或者 subtransaction)組合起來的。fork Effect 和它的字面意思一樣,即創(chuàng)建一個(gè)子 saga 。
4.1、fork
下面寫一個(gè)倒數(shù)的例子,當(dāng)接收到 BEGIN_COUNT 的 action,則開始倒數(shù),而接收到 STOP_COUNT 的 action, 則停止倒數(shù)。
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);} }4.2、call
有阻塞地調(diào)用 saga 或者返回 promise 的函數(shù)。
同樣寫一個(gè)例子:
const project = yield call(fetch, { url: UrlMap.fetchProject }); const members = yield call(fetchMembers, project.id);英文文檔
中文文檔
傳統(tǒng)異步中間件簡(jiǎn)介
在介紹 redux-saga 優(yōu)缺點(diǎn)之前,這里先簡(jiǎn)要介紹傳統(tǒng)的 redux 異步中間件,以便和 redux-saga 做比較。對(duì)傳統(tǒng)異步中間件已經(jīng)充分了解的讀者,可以直接跳到 “redux-saga 優(yōu)缺點(diǎn)分析” 進(jìn)行閱讀。
1. fetch-middleware
使用redux的前端技術(shù)團(tuán)隊(duì)或個(gè)人,大多數(shù)都有一套自己 fetch-middleware,一來可以封裝異步請(qǐng)求的業(yè)務(wù)邏輯,避免重復(fù)代碼,二來可以寫一些公共的異步請(qǐng)求邏輯,比如異常接口數(shù)據(jù)采集、接口緩存、接口處理等等。例如?redux-composable-fetch,redux-api-middleware。
在當(dāng)前 redux 社區(qū)中,fetch-middleware 封裝結(jié)果一般如下:
function loadData(id) {return {url: '/api.json',types: [LOADING_ACTION_TYPE, SUCCESS_ACTION_TYPE, SUCCESS_ACTION_TYPE],params: {id,},}; }值得一提的是,大多數(shù) fetch-middleware 都會(huì)用到一個(gè)小技巧 —— 把最終處理好的 promise 返回出來,以便在 thunk-middleware 中復(fù)用,并組織不同異步過程的先后邏輯。
function loadDetailThunk(id) {return (dispatch) => {// 先請(qǐng)求到 loadData 的結(jié)果,再請(qǐng)求 loadDetaildispatch(loadData(id)).then(result => {const { id: detailId } = result;dispatch(loadDetail(detailId));});}; }這個(gè)技巧在?redux-saga?中也同樣有效。
function* loadDetailSaga(id) {const result = yield put.sync(loadData(id));const { id: detailId } = result;yield put.sync(loadDetail(detailId)); }2. redux-thunk-middleware
redux 中大量應(yīng)用了 thunk 的概念,例如 getState 以延遲執(zhí)行的方式可以始終獲得最新值,redux-thunk 以延遲執(zhí)行的方式把副作用的責(zé)任推卸到用戶身上。
任何異步問題都能在 thunk 中解決。
3.?sequence-middleware
sequence-middleware 用于保證 action 依次執(zhí)行,無論是異步 action 還是普通 aciton ,和 fetch-middleware 配合使用非常方便。
這里可以把每個(gè) action 可以寫成 thunk action,在 thunk 函數(shù)內(nèi)從 store 拿到參數(shù),避免 action 之間的依賴。這樣不管業(yè)務(wù)邏輯有多復(fù)雜,都可以通過用 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?優(yōu)缺點(diǎn)分析
缺點(diǎn)
-
redux-saga 不強(qiáng)迫我們捕獲異常,這往往會(huì)造成異常發(fā)生時(shí)難以發(fā)現(xiàn)原因。因此,一個(gè)良好的習(xí)慣是,相信任何一個(gè)過程都有可能發(fā)生異常。如果出現(xiàn)異常但沒有被捕獲,redux-saga 的錯(cuò)誤棧會(huì)給你一種一臉懵逼的感覺。
-
generator 的調(diào)試環(huán)境比較糟糕,babel 的 source-map 經(jīng)常錯(cuò)位,經(jīng)常要手動(dòng)加 debugger 來調(diào)試。
-
你團(tuán)隊(duì)中使用的其它異步中間件,或許難以和 redux-saga 搭配良好。或許需要花費(fèi)一些代價(jià),用 redux-saga 來重構(gòu)一部分中間件。
優(yōu)點(diǎn)
-
保持 action 的簡(jiǎn)單純粹,aciton 不再像原來那樣五花八門,讓人眼花繚亂。task 的模式使代碼更加清晰。
-
redux-saga 提供了豐富的 Effects,以及 sagas 的機(jī)制(所有的 saga 都可以被中斷),在處理復(fù)雜的異步問題上十分趁手。如果你的應(yīng)用屬于寫操作密集型或者業(yè)務(wù)邏輯復(fù)雜,快讓 redux-saga 來拯救你。
-
擴(kuò)展性強(qiáng)。
-
聲明式的 Effects,使代碼更易測(cè)試,查看詳情。
利用 redux-saga 寫 redux 中間件
用 redux-saga 來寫中間件,可謂事半功倍。這里舉一個(gè)輪詢中間件的例子。
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?在實(shí)踐的沉淀,我已經(jīng)總結(jié)到?redux-saga-sugar,歡迎點(diǎn)贊 ~
總結(jié)
以上是生活随笔為你收集整理的redux-saga 实践总结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: React Router 使用教程
- 下一篇: dva-知识地图