標籤:

Redux 非同步流最佳實踐

我們知道,在 Redux 的世界中,Redux action 返回一個 JS 對象,被 Reducer 接收處理後返回新的 State,這一切看似十分美好。整個過程可以看作是:

view -> actionCreator -> action -> reducer -> newState ->(map) container component

但是真實業務開發我們需要處理非同步請求,比如:請求後台數據,延遲執行某個效果,setTimout, setInterval 等等,所以當 Redux 遇到非同步操作的時候,又該如何處理呢?

首先我們圍繞一個簡單的例子展開,然後通過各種方式將它實現出來,基本效果如下:

不使用中間件處理非同步

這裡我使用的是 CNode 官網的 API,獲取首頁的文章標題,並將他們全部展示出來,並且右邊有個 X 按鈕,點擊 X 按鈕可以將該標題刪除。非同步請求我們使用封裝好的 axios 庫,你可以這樣發起非同步請求:

const response = await axios.get(/api/v1/topics)n

然後在你的 package.json 文件中加上代理欄位

{n "proxy": "https://cnodejs.org",n //...n}n

這樣當你訪問 localhost:3000/api/v1/topics Node 後台會自動幫你轉發請求到 CNode,然後將獲取到的結果返回給你,這個過程對你來說是透明的,這樣能有效避免跨域的問題。

cd asynchronous_without_redux_middleware/nyarn nyarn startn

老規矩,我們先來看看項目結構:

├── actionCreatorn│ └── index.jsn├── actionTypesn│ └── index.jsn├── constantsn│ └── index.jsn├── index.jsn├── reducersn│ └── index.jsn├── storen│ └── index.jsn└── viewsn ├── newsItem.cssn ├── newsItem.jsn └── newsList.jsn

我們在非同步請求時候一共有三種 actionTypes,分別為 FETCH_START, FETCH_SUCCESS, FETCH_FAILURE,這樣對應著視圖就有四種狀態 (constants):INITIAL_STATE, LOADING_STATE, SUCCESS_STATE, FAILURE_STATE。

actionCreator 對 actionTypes 多一層封裝,返回的都還是同步 action,主要的邏輯由視圖組件來完成。

// views/newsList.jsnconst mapDispatchToProps = (dispatch, ownProps) => {n return {n fetchNewsTitle: async() => {n dispatch(actionCreator.fetchStart())n try {n const response = await axios.get(/api/v1/topics)n if(response.status === 200) {n dispatch(actionCreator.fetchSuccess(response.data))n }else {n throw new Error(fetch failure)n }n } catch(e) {n dispatch(actionCreator.fetchFailure())n }n }n }n}n

我們可以看出,在發起非同步請求之前,我們先發起 FETCH_START action,然後開始發起非同步請求,當請求成功之後發起 FETCH_SUCCESS action 並傳遞數據,當請求失敗時發起 FETCH_FAILURE action。

在上面的例子,我們沒有破壞同步 action 這個特性,而是將非同步請求封裝在了具體的業務代碼中,這種直觀的寫法存在著一些問題:

  1. 每當我們發起非同步請求後,我們總是需要寫這樣重複的代碼,手動地處理獲取的數據,其實我們更希望非同步返回後能夠自我消化處理後面的步驟,對於業務層來說,我只需要給出一個信號,比如:FETCH_START action,後續內容就不要再關心了,應用能幫我處理。
  2. 當我們在不同的組件里有同樣的非同步代碼,我們最好將它進行抽象,提取到一個公共的地方進行維護。
  3. 沒有做競態處理:點擊按鈕可以獲取 CNode 標題並呈現,因為非同步請求返回的時間具有不確定性,多次點擊就可能出現後點擊的請求先返回先渲染,而前面點擊的請求後返回覆蓋了最新的請求結果。

通過分析,我們得出需要提取這些邏輯到一個公共的部分,然後簡單調用,後續操作自動完成,就像:

const mapDispatchToProps = (dispatch, ownProps) => {n return {n fetchNewsTitle:() => {n xxx.fetchStart() n }n }n}n

一種思路是將這些非同步調用獨立抽出到一個公共通用非同步操作的文件夾,每個需要調用非同步操作的組件就到這個目錄下獲取需要的函數,但是這樣就存在一個問題,因為需要發起 action 請求,那麼就需要 dispatch 欄位,這就意味著每次調用時候必須顯式地傳入 dispatch 變數,即:

const mapDispatchToProps = (dispatch, ownProps) => {n return {n fetchNewsTitle:() => {n xxx.fetchStart(dispatch) n }n }n}n

這樣寫不夠優雅,不過也不失為一種解決方案,可以進行嘗試,這裡就不展開了。

非同步 Action

此前介紹的都是同步的 action 請求,接下來介紹一下非同步的 action,我們希望在非同步請求的時候,action 能夠這樣處理:

view -> asyncAction -> wait -> action -> reducer -> newState -> container component

這裡 action 不再是同步的,而是具有非同步功能,當然因為依賴於非同步 IO,也會產生副作用。這裡就會存在一個問題,我們需要發起兩次 action 請求,這好像我們又得將 dispatch 對象傳入函數中,顯得不夠優雅。同步和非同步的調用方式截然不同:

  • 同步情況:store.dispatch(actionCreator())
  • 非同步情況: asyncAction(store.dispatch)

好在我們有 Redux 中間件機制能夠幫助我們處理非同步 action,讓 action 不再僅僅處理同步的請求。

Redux-thunk:簡潔直接

Redux 本身只能處理同步的 action,但可以通過中間件來攔截處理其它類型的 action,比如函數(thunk),再用回調觸發普通 action,從而實現非同步處理,在這點上所有 Redux 的非同步方案都是類似的。

首先我們通過 redux-thunk 來改寫我們之前的例子

cd asynchronous_with_redux_thunk/nyarn nyarn startn

首先需要在 store 里注入中間件 redux-thunk。

import { createStore, applyMiddleware } from reduxnimport reducer from ../reducersnimport thunk from redux-thunknn// ...nnexport default createStore(reducer, initValue, applyMiddleware(thunk))n

這樣 redux-thunk 中間件就能夠在 action 傳遞給 reducer 前進行處理。

我們改寫我們 actionCreator 對象,再需要那麼多同步的 action,只需一個方法即可搞定。

// actionCreator/index.jsnimport * as actionTypes from ../actionTypesnimport axios from axiosnnexport default {n fetchNewsTitle: () => {n return async (dispatch, getState) => {n dispatch({n type: actionTypes.FETCH_START,n })n try {n const response = await axios.get(https://cnodejs.org/api/v1/topics)n if(response.status === 200) {n dispatch({n type: actionTypes.FETCH_SUCCESS,n news: response.data.data.map(news => news.title),n })n }else {n throw new Error(fetch failure)n }n } catch(e) {n dispatch({n type: actionTypes.FETCH_FAILUREn })n }n }n },n // ...n}n

這次 fecthNewsTitle 不再簡單返回一個 JS 對象,而是返回一個函數,在函數內可以獲得當前 state 以及 dispatch,然後之前的非同步操作全部封裝在這裡。是 redux-thunk 中間讓 dispatch 不僅能夠處理 JS 對象,也能夠處理一個函數。

之後我們在業務代碼只需調用:

// views/App.jsnconst mapDispatchToProps = (dispatch, ownProps) => {n return {n fetchNewsTitle: () => {n dispatch(actionCreator.fetchNewsTitle())n }n }n}n

自此當以後 redux 需要處理非同步操作的時候,只需將 actionCreator 設為函數,然後在函數里編寫你的非同步邏輯。

redux-thunk 是一個通用的解決方案,其核心思想是讓 action 可以變為一個 thunk ,這樣的話:

  • 同步情況: dispatch(action)
  • 非同步情況: dispatch(thunk)

redux-thunk 看似做了很多工作,實現起來卻是相當簡單:

function createThunkMiddleware(extraArgument) {n return ({ dispatch, getState }) => next => action => {n if (typeof action === function) {n return action(dispatch, getState, extraArgument);n }nn return next(action);n };n}nnconst thunk = createThunkMiddleware();nthunk.withExtraArgument = createThunkMiddleware;nnexport default thunk;n

乍一看有有很多層箭頭函數鏈式調用,這其實跟中間件的機制有關,我們只需要關心,當 action 傳遞到中間件的時候,它會判斷該 action 是不是一個函數,如果函數,則攔截當前的 action,因為在當前的閉包中存在 dispatch 和 getState 變數,將兩個變數傳遞到函數中並執行,這就是我們在 actionCreator 返回函數時候能夠用到 dispatch 和 getState 的關鍵原因。redux-thunk 看到傳遞的 action 是個函數的時候就將其攔截並且執行,這時候這個 action 的返回值已經不再關心,因為它根本沒有被繼續傳遞下去,不是函數的話它就放過這個 action,讓下個中間件去處理它(next(action))

所以我們前面的例子可以理解為:

view -> 函數 action -> redux-thunk 攔截 -> 執行函數並丟棄函數 action -> 一系列 action 操作 -> reducer -> newState -> container componentn

不難理解我們將原本放在公共目錄下的非同步操作封裝在了一個 action,通過中間件的機制讓 action 內部能夠拿到 dispatch 值,從而在 action 中能夠產生更多的同步 action 對象。

redux-thunk 這種方案對於小型的應用來說足夠日常使用,然而對於大型應用來說,你可能會發現一些不方便的地方,對於組合多 action,取消 action,競態判斷的處理就顯然有點兒力不從心,這些東西我們也會在後面進行談到。

redux-thunk 思想很棒,但是其實代碼是有一定的相似,比如其實整個代碼都是針對請求、成功、失敗三部分來處理的,這讓我們自然聯想到 Promise,同樣也是分為 pending、fulfilled、rejected 三種狀態。

Redux-promise:瘦身過度

Promise 代表一種承諾,本用來解決非同步回調地獄問題,首先我們先來看看 redux-promise 中間件的源碼:

import { isFSA } from flux-standard-action;nnfunction isPromise(val) {n return val && typeof val.then === function;n}nnexport default function promiseMiddleware({ dispatch }) {n return next => action => {n if (!isFSA(action)) {n return isPromise(action)n ? action.then(dispatch)n : next(action);n }nn return isPromise(action.payload)n ? action.payload.then(n result => dispatch({ ...action, payload: result }),n error => {n dispatch({ ...action, payload: error, error: true });n return Promise.reject(error);n }n )n : next(action);n };n}n

和 redux-thunk 一樣,我們拋開複雜的鏈式箭頭函數調用,該中間件做的一件事就是判斷 action 或 action.payload 是不是一個 Promise 對象,如果是的話,同樣地攔截等待 Promise 對象 resolve 返回數據之後再調用 dispatch,同樣的,這個 Promise action 也不會被傳遞給 reducer 進行處理,如果不是 Promise 對象就不處理。

所以一個非同步 action 流程就變成了這樣:

view -> Promise action -> redux-promise 攔截 -> 等待 promise resolve -> 將 promise resolve 返回的新的 action(普通) 對象 dispatch -> reducer -> newState -> container componentn

通過 redux-promise 中間件我們可以在編寫 promise action,我們對之前的例子進行修改:

cd asynchronous_with_redux_promise/nyarn nyarn startn

我們修改一下 actionCreator:

// actionCreator/index.jsnexport default {n fetchNewsTitle: () => {n return axios.get(/api/v1/topics).then(response => ({n type: actionTypes.FETCH_SUCCESS,n news: response.data,n })).catch(err => ({n type: actionTypes.FETCH_FAILURE,n }))n },n}n

修改 store 中間件 redux-promise

// store/index.jsnnimport { createStore, applyMiddleware } from reduxnimport reducer from ../reducersnimport reduxPromise from redux-promisennexport default createStore(reducer, initValue, applyMiddleware(reduxPromise))n

效果:沒有 Loading 這個中間狀態

但是如果使用 redux-promise 的話相當於是延後執行了 action,等獲取到結果之後再重新 dispatch action。這麼寫其實有個問題,就是無法發起 FETCH_START action,因為actionCreator 中沒有 dispatch 這個欄位,redux-promise 雖然賦予了 action 延後執行的能力,但是沒有能力發起多 action 請求。

嚴格上來說,我們完全可以寫一個中間件,通過判斷 action 對象上的某個欄位或者什麼其他欄位,代碼如下:

const thunk = ({ dispatch, getState }) => next => action => {ntif(typeof action.async === function) {nt return action.async(dispatch, getState);nt}ntreturn next(action);n}n

如果能夠這樣理解 action 對象,那麼我們也沒有要求 Promise 中間件處理的非同步 action 對象是 Promise 對象,只需要 action 對象謀改革欄位是 Promise 欄位就行,而 action 對象可以擁有其他欄位來包含更多信息。所以我們可以自己編寫一個中間件:

// myPromiseMiddlewarenconst isPromise = (obj) => {n return obj && typeof obj.then === function;n}nnexport default ({ dispatch }) => (next) => (action) => {n const { types, async, ...rest } = actionn if(!isPromise(async) || !(action.types && action.types.length === 3)) {n return next(action)n }n const [PENDING, SUCCESS, FAILURE] = typesn dispatch({n ...rest,n type: PENDING,n })n return action.async.then(n (result) => dispatch({ ...rest, ...result, type: SUCCESS }),n (error) => dispatch({ ...rest, ...error, type: FAILURE })n )n}n

不難理解,中間件接受同樣接收一個 action JS 對象,這個對象需要滿足 async 欄位是 Promise 對象並且 types 欄位長度為 3,否則這不是我們需要的處理的 action 對象,我們傳入的 types 欄位是個數組,分別為 FETCH_START,FETCH_SUCCESS,FETCH_FAILURE,相當於是我們做個一層約定,讓中間件內部去幫我們消化這樣的非同步 action,當 async promise 對象返回之後調用 FETCH_SUCCESS,FETCH_FAILURE action。

我們改寫 actionCreator

// actionCreator/index.jsnexport default {n myFetchNewsTitle: () => {n return {n async: axios.get(/api/v1/topics).then(response => ({n news: response.data,n })),n types: [ actionTypes.FETCH_START, actionTypes.FETCH_SUCCESS, actionTypes.FETCH_FAILURE ]n }n },n}n

這樣寫相當於是我們約定好了格式,然後讓相應地中間件去處理就可以了。但是擴展性較差,適合小型團隊共同開發約定好具體的非同步格式。

Redux-saga:功能強大

redux-saga 也是解決 redux 非同步 action 的一個中間件,不過它與前面的解決方案思路有所不同,它另闢新徑:

  1. redux-saga 完全基於 ES6 的生成器。
  2. 不污染 action,仍使用同步的 action 策略,而是通過監控 action,自動做處理。
  3. 所有帶副作用的操作,如非同步請求,業務控制邏輯代碼都可以放到獨立的 saga 中來。

讓非同步行為成為架構中獨立的一層(稱為 saga),既不在 action creator 中,也不和 reducer 沾邊。

它的出發點是把副作用 (Side effect,非同步行為就是典型的副作用) 看成"線程",可以通過普通的 action 去觸發它,當副作用完成時也會觸發 action 作為輸出。

詳細的文檔說明可以看: Redux-saga Beginner Tutorial

接下來我也會舉很多例子來說明 redux-saga 的優點。

我們老規矩先來改寫我們之前的例子:

cd asynchronous_with_redux_saga/nyarn nyarn startn

首先先來看 actionCreator:

import * as actionTypes from ../actionTypesnexport default {n fetchNewsTitle: () => {n return {n type: actionTypes.FETCH_STARTn }n },nt// ...n}n

是不是變得很乾凈,因為處理非同步的邏輯已經在 creator 裡面了,轉移到 saga 中,我們來看一下 saga 是怎麼寫的。

// sagas/index.jsnnimport { put, takeEvery } from redux-saga/effectsnimport * as actionTypes from ../actionTypesnimport axios from axiosnnfunction* fetchNewsTitle() {n try {n const response = yield axios.get(/api/v1/topics)n if(response.status === 200) {n yield put({n type: actionTypes.FETCH_SUCCESS,n news: response.data,n })n }else {n throw new Error(fetch failure)n }n } catch(e) {n yield put({n type: actionTypes.FETCH_FAILUREn })n }n}nnexport default function* fecthData () {n yield takeEvery(actionTypes.FETCH_START, fetchNewsTitle)n}n

可以發現這裡寫的跟之前寫的非同步操作基本上是一模一樣,上面的代碼不理解,takeEvery 監聽所有的 action,每當發現 action.type === FETCH_START 時執行 fetchNewsTitle 這個函數,注意這裡只是做監聽 action 的動作,並不會攔截 action,這說明 FETCH_START action 仍然會經過 reducer 去處理,剩下 fetchNewsTitle 函數就很好理解,就是執行所謂的非同步操作,這裡的 put 相當於 dispatch。

最後我們需要在 store 裡面使用 saga 中間件

// store/index.jsnimport createSagaMiddleware from redux-saganimport mySaga from ../sagasnnconst sagaMiddleware = createSagaMiddleware()nn// ...nnexport default createStore(reducer, initValue, applyMiddleware(sagaMiddleware))nsagaMiddleware.run(mySaga)n

通過註冊 saga 中間件並且 run 監聽 saga 任務,也就是前面提到的 fecthData。

基於這麼一個簡單的例子,我們可以看到 saga 將所有的帶副作用的操作與 actionCreator 和 reducer 進行分離,通過監聽 action 來做自動處理,相比 async action creator 的方案,它可以保證組件觸發的 action 是純對象。

參考回答:redux-saga 實踐總結

它與 redux-thunk 編寫非同步的方式有著它各自的應用場景,沒有優劣之分,所謂存在即合理。redux-saga 相對於這種方式,具有以下的特點:

  1. 生命周期有所不同,redux-saga 可以理解成一直運行與後台的長時事務,而 redux-thunk 是一個 action,因為 redux-saga 能做的事更多。
  2. redux-saga 有諸多聲明式易測的 Effects,比如無阻塞調用,中斷任務,這些特性在業務邏輯複雜的場景下非常適用。
  3. redux-saga 最具魅力的地方,是它保持了 action 的原義,保持 action 的簡潔,把所有帶副作用的地方獨立開來,這些特性使得其在業務邏輯簡單的場景下,也能保持代碼清晰簡潔。

在我看來:redux-thunk + async/await 的方式學習成本低,比較適合不太複雜的非同步交互場景。對於競態判斷,多重 action 組合,取消非同步等場景下使用則顯得乏力,redux-saga 在非同步交互複雜的場景下仍能夠讓你清晰直觀地編寫代碼,不過學習成本相對較高。

以上我們介紹了三種 redux 非同步方案,其實關於 redux 非同步方案還有很多,比如:redux-observale,通過 rxjs 的方式來書寫非同步請求,也有像 redux-loop 這樣的方式通過在 reducer 上做文章來達到非同步效果。其實方案千千萬萬,各成一派,每種方案都有其適合的場景,結合自己實際的需求來選擇你所使用的 redux 非同步方案才最可貴。

thunk 和 saga 非同步方案對比

對於前面獲取非同步的例子,還沒有結束,它仍存在著一些問題:

  1. 沒有進行防抖處理,如果用戶瘋狂點擊按鈕,那麼將會不斷發起非同步請求,這樣無形之中就對帶寬造成了浪費。
  2. 沒有做競態判斷,點擊按鈕可以獲取 CNode 標題並呈現,因為非同步請求返回的時間具有不確定性,多次點擊就可能出現後點擊的請求先返回先渲染,而前面點擊的請求後返回覆蓋了最新的請求結果。
  3. 沒有做取消處理,是想一下,在某些場景下,在等待的過程中,用戶是有可能取消這個非同步操作的,這時候就不呈現結果了。

下面我們將重新改寫一個例子,分別用 redux-thunk 和 redux-saga 對其進行處理上述的問題,並進行比較。

我們要做的例子效果如下:

  1. 有兩個按鈕用來模擬非同步請求,分別在 5s 和 1s 內響應數據,我們需要保證按鈕點擊的順序性,即當 5s 後數據返回時不會覆蓋掉最新數值 1000,保證頁面上顯示的數據永遠是最後一次點擊獲取到的數據。
  2. 防抖處理,在 1000ms 內再次點擊按鈕不會進行響應。

cd race_with_redux_thunk/nyarn nyarn startn

查看 actionCreator:

// actions/index.jsnimport * as actionTypes from ../actionTypesnlet nextId = 0nlet prev = 0nexport const updateData = (ms) => {n return (dispatch) => {n let id = ++nextIdn let now = + new Date()n if(now - prev < 1000) {n return;n }n prev = now;nn const checkLast = (action) => {n if(id === nextId) {n dispatch(action)n }n }n setTimeout(() => {n checkLast({n type: actionTypes.UPDATE_DATA,n payload: ms, n })n }, ms)n }n}n

  1. 競態處理:可以通過在 actionCreator 中添加模塊變數 nextId,在執行函數的時候生成一個 id 與當前 nextId 值相同,最後當數據返回後,判斷當前 id 是否與 nextId 相同值,如果相同,則證明這次操作是最後一次操作,從而保證該請求為最後一次請求。
  2. 防抖處理:另外通過變數 prev 來記錄上次點擊的時間,通過與當前時間之差與 1s 進行判斷來決定是否執行後續操作,並且會更新 prev 值。

如果是 redux-saga 重寫這個例子,那麼又是什麼效果呢?

cd race_with_redux_saga/nyarn nyarn startn

首先 reducer 就不用那麼麻煩,它只要給一個信號就可以了

// actions/index.jsnimport * as actionTypes from ../actionTypesnnexport const updateData = (ms) => {n return {n type: actionTypes.INITIAL,n msn }n}n

重點是監聽 INITIAL 的 saga 任務

// sagas/index.jsnimport { put, call, take, fork, cancel } from redux-saga/effectsnimport * as actionTypes from ../actionTypesnnconst delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))nnfunction* asyncAction({ ms }) {n let promise = new Promise((resolve) => {n setTimeout(() => {n resolve(ms)n }, ms)n })n let payload = yield promisen yield put({n type: actionTypes.UPDATE_DATA,n payload: payloadn })n}nnexport default function* watchAsyncAction() {n let task n while(true) {n const action = yield take(actionTypes.INITIAL)n if(task) {n yield cancel(task)n }n task = yield fork(asyncAction, action)n yield call(delay, 1000)n }n}n

watchAsyncAction 用於監聽傳遞過來的 action 類型,雖然通過 while(true) 來寫,不過因為是生成器並不具備執行完代碼的能力,它代表會一直重複循環地監聽每次 action。

take 監聽 INITIAL 類型的 action,首先判斷之前有沒有任務未執行完畢,如果有,則取消該任務,從而保證競態判斷,然後通過 fork 非阻塞調用,最後停頓一秒,call 代表阻塞調用,在這段時間內,該 saga 任務不再處理新進來的 action,所以在這段時間所有的 INITIAL action 都將會被忽略,從而進行防抖處理,put 相當於 dispatch 操作。

通過這個簡單例子的對比,我們可以看出,redux-saga 更加靈活,寫起來比較優雅,代碼可讀性更強,你可以比較清晰地理解代碼的行為,當然相應地你也要熟悉更多的基本概念,學習成本較高。

另外值得一提的是因為 redux-saga 基於 ES6 的生成器,可以執行和歸還函數的控制權,所以其可以處理更加複雜的業務場景,具有強大的非同步流程式控制制。

最後我們再通過一個例子來對比 thunk 和 saga 在書寫上的差異,例子效果如下:

點擊按鈕可以每秒增加 1,可以再次點擊進行取消增加計數器,也可以通過 5s 後自動取消增加計數器。

我們先來看看 redux-thunk 要如何處理:

cd cancellable_counter_with_redux_thunk/nyarn nyarn startn

在 action creator 中,我們需要創建兩個定時器用來觸發兩個數字的更新。

import {n INCREMENT,n COUNTDOWN,n COUNTDOWN_CANCEL,n COUNTDOWN_TERMIMATEDn} from ../actionTypesnnlet incrementTimernlet countdownTimernnconst action = type => ({ type })nnexport const increment = () => action(INCREMENT)nnexport const terminateCountDown = () => (dispatch) => {n clearInterval(incrementTimer)n clearInterval(countdownTimer)n dispatch(action(COUNTDOWN_TERMIMATED))n}nnexport const cancelCountDown = () => (dispatch) => {n clearInterval(incrementTimer)n clearInterval(countdownTimer)n dispatch(action(COUNTDOWN_CANCEL))n}nnexport const incrementAsync = (time) => (dispatch) => {n incrementTimer = setInterval(() => {n dispatch(increment())n }, 1000)n dispatch({n value: time,n type: COUNTDOWN,n })n countdownTimer = setInterval(() => {n time--n if(time <= 0) {n dispatch(cancelCountDown())n }else {n dispatch({n value: time,n type: COUNTDOWN,n })n }n }, 1000)n}n

incrementAsync 開啟兩個計時器,用於增加計數器和倒計時,terminateCountDown 為人工觸發按鈕導致兩個定時器被清除,而在 countdownTimer 定時器內部,隨著時間流逝,當 time 小於 0 時觸發 cancelCountDown,取消所有定時器。

我們可以看出,redux-thunk 在處理這類非同步流程式控制制時候有點力不從心,需要創建多個定時器來並行地改變數據,當場景更加複雜的時候代碼就顯得有點亂,可讀性較差。

再用 redux-saga 改寫剛才的例子:

cd cancellable_counter_with_redux_saga/nyarn nyarn startnn// sagas/index.jsnimport { n INCREMENT_ASYNC, n INCREMENT, n COUNTDOWN,n COUNTDOWN_TERMIMATED,n COUNTDOWN_CANCEL,n} from ../actionTypesnimport { take, put, cancelled, call, race, cancel, fork } from redux-saga/effectsnimport { delay } from redux-sagannconst action = type => ({ type })nnfunction* incrementAsync() {n while(true) {n yield call(delay, 1000)n yield put(action(INCREMENT))n }n}nnfunction* countdown({ ms }) {n let task = yield fork(incrementAsync)n try {n while(true) { n yield put({n type: COUNTDOWN,n value: ms--,n }) n yield call(delay, 1000)n if(ms <= 0) {n break;n }n }n } finally {n if(!(yield cancelled())) {n yield put(action(COUNTDOWN_CANCEL))n }n yield cancel(task)n }n}nnexport default function* watchIncrementAsync() {n while(true) {n const action = yield take(INCREMENT_ASYNC)n yield race([n call(countdown, action),n take(COUNTDOWN_TERMIMATED)n ])n }n}n

關於 Redux 非同步控制就講到這裡,希望大家有所收穫!

推薦閱讀:

前端開發每周閱讀清單:PWA 將與安卓原生平起平坐
ReactEurope 2016 小記 - 下
如何初始化整個redux應用?
The Redux Journey 翻譯及分析(上)
為什麼 React 推崇 HOC 和組合的方式,而不是繼承的方式來擴展組件?

TAG:React |