書籍完整目錄
3.5 compose redux sages
基於 redux-thunk 的實現特性,可以做到基於 promise 和遞歸的組合編排,而 redux-saga 提供了更容易的更高級的組合編排方式(當然這一切要歸功於 Generator 特性),這一節的主要內容為:
-
基於 take Effect 實現更自由的任務編排
-
fork 和 cancel 實現非阻塞任務
-
Parallel 和 Race 任務
-
saga 組合 yield* saga
-
channels
3.5.1 基於 take Effect 實現更自由的任務編排
前面我們使用過 takeEvery helper, 其實底層是通過 take effect 來實現的。通過 take effect 可以實現很多有趣的簡潔的控制。
如果用 takeEvery 實現日誌打印,我們可以用:
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 能極大的簡化交互邏輯處理
3.5.2 fork 和 cancel 實現非阻塞任務
在提非阻塞之前肯定要先要説明什麼叫阻塞的代碼。我們看一下下面的例子:
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})
}
}
3.5.3 Parallel 和 Race 任務
Parallel
基於 generator 的特性,下面的代碼會按照順序執行
const users = yield call(fetch, '/users'),
repos = yield call(fetch, '/repos')
為了優化效率,可以讓兩個任務並行執行
const [users, repos] = yield [
call(fetch, '/users'),
call(fetch, '/repos')
]
Race
某些情況下可能會對優先完成的任務進行處理,一個很常見的例子就是超時處理,當請求一個 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 。
3.5.4 yield* 組合 saga
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))
}
3.5.5 channels
通過 actionChannel 實現緩存區
先看如下的例子:
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 和外部事件連接起來
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')
}
}
}
通過 channel 在 saga 之間通信
除了 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
}
}
參考鏈接
-
http://yelouafi.github.io/redux-saga/docs/advanced/index.html
-
http://gajus.com/blog/2/the-definitive-guide-to-the-javascript-generators#understanding-the-execution-flow