書籍完整目錄javascript
基於 redux-thunk 的實現特性,能夠作到基於 promise 和遞歸的組合編排,而 redux-saga 提供了更容易的更高級的組合編排方式(固然這一切要歸功於 Generator 特性),這一節的主要內容爲:html
基於 take Effect 實現更自由的任務編排java
fork 和 cancel 實現非阻塞任務git
Parallel 和 Race 任務github
saga 組合 yield* sagaredux
channelssegmentfault
前面咱們使用過 takeEvery helper, 其實底層是經過 take effect 來實現的。經過 take effect 能夠實現不少有趣的簡潔的控制。api
若是用 takeEvery 實現日誌打印,咱們能夠用:promise
import { takeEvery } from 'redux-saga' import { put, select } from 'redux-saga/effects' function* watchAndLog() { yield* takeEvery('*', function* logger(action) { const state = yield select() console.log('action', action) console.log('state after', state) }) }
使用使用 take 事後能夠改成:緩存
import { take } from 'redux-saga/effects' import { put, select } from 'redux-saga/effects' function* watchAndLog() { while (true) { const action = yield take('*') const state = yield select() console.log('action', action) console.log('state after', state) } }
while(true) 的執行並不是是死循環,而只是不斷的生成迭代項而已,take Effect 在沒有獲取到對象的 action 時,會中止執行,直到接收到 action 纔會執行後面的代碼,而後從新等待
take 和 takeEvery 最大的區別在於 take 是主動獲取 action ,至關於 action = getNextAction() , 而 takeEvery 是消息推送。
基於主動獲取的,能夠作到更自由的控制,以下面的兩個例子:
完成了三個任務後,提示恭喜
import { take, put } from 'redux-saga/effects' function* watchFirstThreeTodosCreation() { for (let i = 0; i < 3; i++) { const action = yield take('TODO_CREATED') } yield put({type: 'SHOW_CONGRATULATION'}) }
登陸和登出邏輯能夠放在同一個函數內共享變量
function* loginFlow() { while (true) { yield take('LOGIN') // ... perform the login logic yield take('LOGOUT') // ... perform the logout logic } }
take 最難以想象的地方就是,將 異步的任務用同步的方式來編排 ,使用好 take 能極大的簡化交互邏輯處理
在提非阻塞以前確定要先要說明什麼叫阻塞的代碼。咱們看一下下面的例子:
function* generatorFunction() { console.log('start') yield take('action1') console.log('take action1') yield call('api') console.log('call api') yield put({type: 'SOME_ACTION'}) console.log('put blabla') }
由於 generator 的特性,必需要等到 take 完成纔會輸出 take action1, 同理必需要等待 call api 完成纔會輸出 call api, 這就是咱們所說的阻塞。
那阻塞會形成什麼問題呢?見下面的例子:
一個登陸的例子(這是一段有問題的代碼,能夠先研究一下這段代碼問題出在哪兒)
import { take, call, put } from 'redux-saga/effects' import Api from '...' function* authorize(user, password) { try { const token = yield call(Api.authorize, user, password) yield put({type: 'LOGIN_SUCCESS', token}) return token } catch(error) { yield put({type: 'LOGIN_ERROR', error}) } } function* loginFlow() { while (true) { const {user, password} = yield take('LOGIN_REQUEST') const token = yield call(authorize, user, password) if (token) { yield call(Api.storeItem, {token}) yield take('LOGOUT') yield call(Api.clearItem, 'token') } } }
咱們先來分析一下 loginFlow 的流程:
經過 take effect 監聽 login_request action
經過 call effect 來異步獲取 token (call 不只能夠用來調用返回 Promise 的函數,也能夠用它來調用其餘 Generator 函數,返回結果爲調用 Generator return 值)
成功(有 token)
1 事後異步存儲 token
等待 logout action
logout 事件觸發後異步清除 token
而後回到第 0 步
失敗(token === undefined) 回到第 0 步
其中的問題:
一個隱藏的陷阱,在調用 authorize 的時候,若是用戶點擊了頁面中的 logout 按鈕將會沒有反應(此時尚未執行 take('LOGOUT')) , 也就是被 authorize 阻塞了。
redux-sage 提供了一個叫 fork
的 Effect,能夠實現非阻塞的方式,下面咱們從新設計上面的登陸例子:
function* authorize(user, password) { try { const token = yield call(Api.authorize, user, password) yield put({type: 'LOGIN_SUCCESS', token}) } catch(error) { yield put({type: 'LOGIN_ERROR', error}) } } function* loginFlow() { while(true) { const {user, password} = yield take('LOGIN_REQUEST') yield fork(authorize, user, password) yield take(['LOGOUT', 'LOGIN_ERROR']) yield call(Api.clearItem('token')) } }
token 的獲取放在了 authorize saga 中,由於 fork 是非阻塞的,不會返回值
authorize 中的 call 和 loginFlow 中的 take 並行調用
這裏 take 了兩個 action , take 能夠監聽併發的 action ,無論哪一個 action 觸發都會執行 call(Api.clearItem...) 並回到 while 開始
在用戶觸發 logout 以前, 若是 authorize 成功,那麼 loginFlow 會等待 LOGOUT action
在用戶觸發 logout 以前, 若是 authorize 失敗,那麼 loginFlow 會 take('LOGIN_ERROR')
若是在用戶觸發 logout 的時候,authorize 尚未執行完成,那麼會執行後面的語句並回到 while 開始
這個過程當中的問題是若是用戶觸發 logout 了,無法中止 call api.authorize , 並會觸發 LOGIN_SUCCESS 或者 LOGIN_ERROR action 。
redux-saga 提供了 cancel Effect,能夠 cancel 一個 fork task
import { take, put, call, fork, cancel } from 'redux-saga/effects' // ... function* loginFlow() { while (true) { const {user, password} = yield take('LOGIN_REQUEST') // fork return a Task object const task = yield fork(authorize, user, password) const action = yield take(['LOGOUT', 'LOGIN_ERROR']) if (action.type === 'LOGOUT') yield cancel(task) yield call(Api.clearItem, 'token') } }
cancel 的了某個 generator, generator 內部會 throw 一個錯誤方便捕獲,generator 內部 能夠針對不一樣的錯誤作不一樣的處理
import { isCancelError } from 'redux-saga' import { take, call, put } from 'redux-saga/effects' import Api from '...' function* authorize(user, password) { try { const token = yield call(Api.authorize, user, password) yield put({type: 'LOGIN_SUCCESS', token}) return token } catch(error) { if(!isCancelError(error)) yield put({type: 'LOGIN_ERROR', error}) } }
基於 generator 的特性,下面的代碼會按照順序執行
const users = yield call(fetch, '/users'), repos = yield call(fetch, '/repos')
爲了優化效率,可讓兩個任務並行執行
const [users, repos] = yield [ call(fetch, '/users'), call(fetch, '/repos') ]
某些狀況下可能會對優先完成的任務進行處理,一個很常見的例子就是超時處理,當請求一個 API 超過多少時間事後執行特定的任務。
eg:
import { race, take, put } from 'redux-saga/effects' import { delay } from 'redux-saga' function* fetchPostsWithTimeout() { const {posts, timeout} = yield race({ posts: call(fetchApi, '/posts'), timeout: call(delay, 1000) }) if (posts) put({type: 'POSTS_RECEIVED', posts}) else put({type: 'TIMEOUT_ERROR'}) }
這裏默認使用到了 race 的一個特性,若是某一個任務成功了事後,其餘任務都會被 cancel 。
yield* 是 generator 的內關鍵字,使用的場景是 yield 一個 generaor。
yield* someGenerator 至關於把 someGenerator 的代碼放在當前函數執行,利用這個特性,能夠組合使用 saga
function* playLevelOne() { ... } function* playLevelTwo() { ... } function* playLevelThree() { ... } function* game() { const score1 = yield* playLevelOne() put(showScore(score1)) const score2 = yield* playLevelTwo() put(showScore(score2)) const score3 = yield* playLevelThree() put(showScore(score3)) }
先看以下的例子:
import { take, fork, ... } from 'redux-saga/effects' function* watchRequests() { while (true) { const {payload} = yield take('REQUEST') yield fork(handleRequest, payload) } } function* handleRequest(payload) { ... }
這個例子是典型的 watch -> fork
,也就是每個 REQEST 請求都會被併發的執行,如今若是有需求要求 REQUEST 一次只能執行一個,這種狀況下可使用到 actionChannel
經過 actionChannel 修改上例子
import { take, actionChannel, call, ... } from 'redux-saga/effects' function* watchRequests() { // 爲 REQUEST 建立一個 actionChannel 至關於一個緩衝區 const requestChan = yield actionChannel('REQUEST') while (true) { // 重 channel 中取一個 action const {payload} = yield take(requestChan) // 使用非阻塞的方式調用 request yield call(handleRequest, payload) } } function* handleRequest(payload) { ... }
channel 能夠設置緩衝區的大小,若是隻想處理最近的5個 action 能夠以下設置
import { buffers } from 'redux-saga' const requestChan = yield actionChannel('REQUEST', buffers.sliding(5))
eventChannel 不一樣於 actionChannel,actionChannel 是一個 Effect ,而 eventChannel 是一個工廠函數,能夠建立一個自定義的 channel
下面建立一個倒計時的 channel 工廠
import { eventChannel, END } from 'redux-saga' function countdown(secs) { return eventChannel(emitter => { const iv = setInterval(() => { secs -= 1 if (secs > 0) { emitter(secs) } else { // 結束 channel emitter(END) clearInterval(iv) } }, 1000); // 返回一個 unsubscribe 方法 return () => { clearInterval(iv) } } ) }
經過 call 使用建立 channel
export function* saga() { const chan = yield call(countdown, value) try { while (true) { // take(END) 會致使直接跳轉到 finally let seconds = yield take(chan) console.log(`countdown: ${seconds}`) } } finally { // 支持外部 cancel saga if (yield cancelled()) { // 關閉 channel chan.close() console.log('countdown cancelled') } else { console.log('countdown terminated') } } }
除了 eventChannel 和 actionChannel,channel 能夠不用鏈接任何事件源,直接建立一個空的 channel,而後手動的 put 事件到 channel 中
以上面的 watch->fork 爲基礎,需求改成 ,須要同時併發 3 個request 請求執行:
import { channel } from 'redux-saga' import { take, fork, ... } from 'redux-saga/effects' function* watchRequests() { // 建立一個空的 channel const chan = yield call(channel) // fork 3 個 worker saga for (var i = 0; i < 3; i++) { yield fork(handleRequest, chan) } while (true) { // 等待 request action const {payload} = yield take('REQUEST') // put payload 到 channel 中 yield put(chan, payload) } } function* handleRequest(chan) { while (true) { const payload = yield take(chan) // process the request } }