在React/Redux應用中使用Sagas管理異步操作
參考這篇文章,譯文可能先幼稚,參考看看吧.redux-saga本身的一些概念很難
可以也參考 redux-saga的文檔
Redux是一個和Flux類似的框架,在React社區中增長很快.他通過使用單個狀態的原子性和純函數式reduce來更新state,從而加強單向數據流,減小了數據操作的復雜性.
對于我,配置React+Flux一直是一根肉中刺,包括有Action creators的協作,異步操作也是非常的棘手.解決辦法是在React組件中使用生命周期方法(life cycle),例如componentDidupdate,componentWillUpdate等等,在action creators中通過返回thunks(類似Promise對象)對象也可以工作.但是這些方法似乎在有些條件下會不太好使用.
我了更好的表達我的意思,我們來看看一個簡單的Timer App. 整個APP的代碼可在這里.
計時器APP
這個app允許使用者開始和停止一個定時器,也可以重置它.
我們可以把這個app可以看做一個在stopped和Running兩個狀態之間相互轉變的有限狀態機(finite machine).參見下面的簡圖.當timer在Running狀態時,狀態機會每一秒種更新app一次.
讓我首先把app的基本設置看一下,然后我們演示一下怎么在action creators和React組件之外使用sagas幫助管理異步操作(side-effects).
Actions
在模塊中有四個actions
- START-計時器改變為運行狀態.
- TICK-時鐘每個滴答以后遞增定時器
- STOP-計時器改變為停止狀態
- RESET-復位定時器
// actions.js,四種action
export default { start: () => ({ type: 'START' })
, tick: () => ({ type: 'TICK' })
, stop: () => ({ type: 'STOP' })
, reset: () => ({ type: 'RESET' })
};
狀態模型和Reducer
計時器的狀態由兩部分屬性組成:status和seconds
type Model = {
status: string;
seconds: number;
}
status是運行和停止兩個狀態,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).視圖渲染當前的時間和狀態.于此同時在用戶點擊Reset,Start或Stop按鈕的時候喚醒相應的回調函數.
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的狀態是在運行和停止之間轉變,但是還沒有周期性改變定時器的機制.
在典型的Redux+React的app中,有兩種方法可以處理周期性的更新.
- 視圖周期性的回調action creator
- action creator返回一個thunk對象,這個對象周期性的dispatch TICK actions.
解決方案1:讓視圖dispatch更新
對于#1方案,視圖必須等待定時器的狀態從停止轉變為開始才能開始周期性的action派發.意思是我們不得不使用有狀態的組件.
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);
}
// ...
}
這種處理方式可以工作,但是這會使視圖變得滿是狀態,而且也會不純凈.另一個問題是我們的組件現在不僅僅需要渲染HTML,捕獲用戶的交互操作還要承擔更多的工作.這種方式里引入致異步操作會使視圖和應用作為一個整體,很難理清.在計時器這個app里面可能還不是什么問題.但是如果在一個大型的應用中,你可能想把異步操作放到整個應用的外面.
所以使用Thunks對象怎么樣?
解決方案2:在Action Creator中使用Thunks對象
替代方案1在視圖中進行操作,可以在我們的action creator中使用thunks.改變一下start的action creator
export default {
start: () => (
(dispatch, getState) => {
// This transitions state to Running
dispatch({ type: 'START' });
//上面的注釋的譯文:dispatch({type:'START'})改變狀態為Running
// Check every 1 second if we are still Running.
// If so, then dispatch a `TICK`, otherwise stop
// the timer.
//每一秒種檢測一下狀態是不是還是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回調函數被調用.接著只要計時器只要還在工作,每一秒鐘將會dispatch一個TICK action.
在action creator中使用的方式一個問題是action creator現在要做很多的事情.測試也是一個很難完成的任務,因為沒有返回任何數據.
最好的解決辦法是:使用Sagas去管理計時器.
redux-sagas重新定義side-effects為Effects
.Effects
由Sagas生成.sagas的概念據我所知來自CQRS和Event Sourcing世界.有許多討論爭論sagas到底是什么,但是你可以認為sagas是和系統交互的永久線程:
- 對系統中的acion dispach做出反應
- 往系統中Dispatch新的actions
- 可以使用內部機制在沒有外部actions的情況下自我復蘇.例如周期性的蘇醒.
在redux-saga里,一個saga就是一個生成器函數(generator function
),可以在系統內無限期運行.當特定action被dispatch時,saga就可以被喚醒.saga也可以繼續dispatch額外的actions,也可以接入程序的單一狀態樹.
例如,我們想在計時器運行的時候,周期性的dispatch
TICKS
.看看下面的操作:
function* runTimer(getState) {
// The sagasMiddleware will start running this generator.
//sagas中間件將開始運行這個生成器函數.
// Wake up when user starts timer.
//當用戶開始計時器的時候喚醒.
while(yield take('START')) {
while(true) {
// This side effect is not run yet, so it can be treated
//side effect 沒有運行,所以可以看做數據
// 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.
//如果計時器沒有運行的話,就進入休眠狀態等待計時器的重新工作
} else {
break;
}
}
}
}
正如你所見到的,一個saga使用普通的JavaScript控制流程來構建協作side-effects和action creators的過程.take函數在START
action被dispatch的時候喚醒.call函數允許我們創建類似于待辦事項的等待效果.(就是類似list-todo,已經在日程表中列出,但還沒有執行的任務)
通過使用saga,我們可以保持視圖和action creator成為純函數.saga使我們可以使用類似javascript構造函數的方式創建state轉變的模型.
包裝
Sagas是系統內管理side-effects的途徑.當你的應用中需要長時間運行的進程來協作多個action creators和side-effects的時候,Sagas將會非常的合適.
Sagas不僅對actions做出響應,而且對內部機制也可以做出響應(例如,時間依賴的effects).Sagas尤其有用,特別是你需要在正常的Flux流程之外管理side-effects的時候.例如,一個用戶的交互操作可能會有更多的action產生,但是這些actions卻不需要用戶更多的操作.
最后,當你需要一個無限狀態機模型的時候,sagas也值得一試.
如果你想看看Timer app的完整代碼,看看這里.
你準備嘗試sagas了嗎?好了,有什么想法呢?