在React/Redux應(yīng)用中使用Sagas管理異步操作
參考這篇文章,譯文可能先幼稚算色,參考看看吧.redux-saga本身的一些概念很難
可以也參考 redux-saga的文檔
Redux是一個和Flux類似的框架,在React社區(qū)中增長很快.他通過使用單個狀態(tài)的原子性和純函數(shù)式reduce來更新state,從而加強單向數(shù)據(jù)流,減小了數(shù)據(jù)操作的復(fù)雜性.
對于我,配置React+Flux一直是一根肉中刺,包括有Action creators的協(xié)作,異步操作也是非常的棘手.解決辦法是在React組件中使用生命周期方法(life cycle),例如componentDidupdate,componentWillUpdate等等,在action creators中通過返回thunks(類似Promise對象)對象也可以工作.但是這些方法似乎在有些條件下會不太好使用.
我了更好的表達(dá)我的意思挫以,我們來看看一個簡單的Timer App. 整個APP的代碼可在這里.
計時器APP
這個app允許使用者開始和停止一個定時器,也可以重置它.
我們可以把這個app可以看做一個在stopped和Running兩個狀態(tài)之間相互轉(zhuǎn)變的有限狀態(tài)機(finite machine).參見下面的簡圖.當(dāng)timer在Running狀態(tài)時,狀態(tài)機會每一秒種更新app一次.
讓我首先把app的基本設(shè)置看一下,然后我們演示一下怎么在action creators和React組件之外使用sagas幫助管理異步操作(side-effects).
Actions
在模塊中有四個actions
- START-計時器改變?yōu)檫\行狀態(tài).
- TICK-時鐘每個滴答以后遞增定時器
- STOP-計時器改變?yōu)橥V範(fàn)顟B(tài)
- RESET-復(fù)位定時器
// actions.js,四種action
export default { start: () => ({ type: 'START' })
, tick: () => ({ type: 'TICK' })
, stop: () => ({ type: 'STOP' })
, reset: () => ({ type: 'RESET' })
};
狀態(tài)模型和Reducer
計時器的狀態(tài)由兩部分屬性組成:status和seconds
type Model = {
status: string;
seconds: number;
}
status是運行和停止兩個狀態(tài),seconds只要定時器開始計時就開始累積.
Reducer的實際代碼如下
// reducer.js
const INITIAL_STATE = { status: 'Stopped'
, seconds: 0
};
export default (state = INITIAL_STATE, action = null) => {
switch (action.type) {
case 'START':
return { ...state, status: 'Running' };
case 'STOP':
return { ...state, status: 'Stopped' };
case 'TICK':
return { ...state, seconds: state.seconds + 1 };
case 'RESET':
return { ...state, seconds: 0 };
default:
return state;
}
};
Timer的UI視圖
視圖(view)是比較單純的的诫惭,所以和異步操作是完全隔絕的(side-effects free).視圖渲染當(dāng)前的時間和狀態(tài).于此同時在用戶點擊Reset,Start或Stop按鈕的時候喚醒相應(yīng)的回調(diào)函數(shù).
export const Timer = ({ start, stop, reset, state }) => (
<div>
<p>
{ getFormattedTime(state) } ({ state.status })
</p>
<button
disabled={state.status === 'Running'}
onClick={() => reset()}>
Reset
</button>
<button
disabled={state.status === 'Running'}
onClick={() => start()}>
Start
</button>
<button
disabled={state.status === 'Stopped'}
onClick={stop}>
Stop
</button>
</div>
);
問題:怎么處理周期性的更新操作.
目前app的狀態(tài)是在運行和停止之間轉(zhuǎn)變,但是還沒有周期性改變定時器的機制.
在典型的Redux+React的app中,有兩種方法可以處理周期性的更新.
- 視圖周期性的回調(diào)action creator
- action creator返回一個thunk對象,這個對象周期性的dispatch TICK actions.
解決方案1:讓視圖dispatch更新
對于#1方案,視圖必須等待定時器的狀態(tài)從停止轉(zhuǎn)變?yōu)殚_始才能開始周期性的action派發(fā).意思是我們不得不使用有狀態(tài)的組件.
class Timer extends Component {
componentWillReceiveProps(nextProps) {
const { state: { status: currStatus } } = this.props;
const { state: { status: nextStatus } } = nextProps;
if (currState === 'Stopped' && nextState === 'Running') {
this._startTimer();
} else if (currState === 'Running' && nextState === 'Stopped') {
this._stopTimer();
}
}
_startTimer() {
this._intervalId = setInterval(() => {
this.props.tick();
}, 1000);
}
_stopTimer() {
clearInterval(this._intervalId);
}
// ...
}
這種處理方式可以工作,但是這會使視圖變得滿是狀態(tài),而且也會不純凈.另一個問題是我們的組件現(xiàn)在不僅僅需要渲染HTML,捕獲用戶的交互操作還要承擔(dān)更多的工作.這種方式里引入致異步操作會使視圖和應(yīng)用作為一個整體桩了,很難理清.在計時器這個app里面可能還不是什么問題.但是如果在一個大型的應(yīng)用中附帽,你可能想把異步操作放到整個應(yīng)用的外面.
所以使用Thunks對象怎么樣?
解決方案2:在Action Creator中使用Thunks對象
替代方案1在視圖中進(jìn)行操作,可以在我們的action creator中使用thunks.改變一下start的action creator
export default {
start: () => (
(dispatch, getState) => {
// This transitions state to Running
dispatch({ type: 'START' });
//上面的注釋的譯文:dispatch({type:'START'})改變狀態(tài)為Running
// Check every 1 second if we are still Running.
// If so, then dispatch a `TICK`, otherwise stop
// the timer.
//每一秒種檢測一下狀態(tài)是不是還是Running,如果是的
//話,dispatch ‘TICK’aciton.否則就停止計時器
const intervalId = setInterval(() => {
const { status } = getState();
if (status === 'Running') {
dispatch({ type: 'TICK' });
} else {
clearInterval(intervalId);
}
}, 1000);
}
)
// ...
};
Start action creator將會dispatch一個START action,只要start回調(diào)函數(shù)被調(diào)用.接著只要計時器只要還在工作井誉,每一秒鐘將會dispatch一個TICK action.
在action creator中使用的方式一個問題是action creator現(xiàn)在要做很多的事情.測試也是一個很難完成的任務(wù),因為沒有返回任何數(shù)據(jù).
最好的解決辦法是:使用Sagas去管理計時器.
redux-sagas重新定義side-effects為Effects
.Effects
由Sagas生成.sagas的概念據(jù)我所知來自CQRS和Event Sourcing世界.有許多討論爭論sagas到底是什么,但是你可以認(rèn)為sagas是和系統(tǒng)交互的永久線程:
- 對系統(tǒng)中的acion dispach做出反應(yīng)
- 往系統(tǒng)中Dispatch新的actions
- 可以使用內(nèi)部機制在沒有外部actions的情況下自我復(fù)蘇.例如周期性的蘇醒.
在redux-saga里,一個saga就是一個生成器函數(shù)(generator function
),可以在系統(tǒng)內(nèi)無限期運行.當(dāng)特定action被dispatch時,saga就可以被喚醒.saga也可以繼續(xù)dispatch額外的actions,也可以接入程序的單一狀態(tài)樹.
例如,我們想在計時器運行的時候,周期性的dispatch
TICKS
.看看下面的操作:
function* runTimer(getState) {
// The sagasMiddleware will start running this generator.
//sagas中間件將開始運行這個生成器函數(shù).
// Wake up when user starts timer.
//當(dāng)用戶開始計時器的時候喚醒.
while(yield take('START')) {
while(true) {
// This side effect is not run yet, so it can be treated
//side effect 沒有運行,所以可以看做數(shù)據(jù)
// as data, making it easier to test if needed.
//這樣測試比較容易一點
yield call(wait, ONE_SECOND);
// Check if the timer is still running.
//檢測計時器是否運行
// If so, then dispatch a TICK.
//如果計時器運行的話,就dispatch一個TICK
if (getState().status === 'Running') {
yield put(actions.tick());
// Otherwise, go idle until user starts the timer again.
//如果計時器沒有運行的話,就進(jìn)入休眠狀態(tài)等待計時器的重新工作
} else {
break;
}
}
}
}
正如你所見到的,一個saga使用普通的JavaScript控制流程來構(gòu)建協(xié)作side-effects和action creators的過程.take函數(shù)在START
action被dispatch的時候喚醒.call函數(shù)允許我們創(chuàng)建類似于待辦事項的等待效果.(就是類似list-todo,已經(jīng)在日程表中列出,但還沒有執(zhí)行的任務(wù))
通過使用saga,我們可以保持視圖和action creator成為純函數(shù).saga使我們可以使用類似javascript構(gòu)造函數(shù)的方式創(chuàng)建state轉(zhuǎn)變的模型.
包裝
Sagas是系統(tǒng)內(nèi)管理side-effects的途徑.當(dāng)你的應(yīng)用中需要長時間運行的進(jìn)程來協(xié)作多個action creators和side-effects的時候,Sagas將會非常的合適.
Sagas不僅對actions做出響應(yīng),而且對內(nèi)部機制也可以做出響應(yīng)(例如,時間依賴的effects).Sagas尤其有用蕉扮,特別是你需要在正常的Flux流程之外管理side-effects的時候.例如,一個用戶的交互操作可能會有更多的action產(chǎn)生,但是這些actions卻不需要用戶更多的操作.
最后,當(dāng)你需要一個無限狀態(tài)機模型的時候,sagas也值得一試.
如果你想看看Timer app的完整代碼,看看這里.
你準(zhǔn)備嘗試sagas了嗎?好了,有什么想法呢颗圣?