背景:
redux-saga
主要用來處理異步的
actions
。
把
action->reducer
的過程變為
action->中間件->reducer
由于團隊内部的
saga
使用不規範,是以輸出一些saga通用使用。
準備:
安裝
$ yarn add react-saga
引入
//main.js
import { createStore,applyMiddleware } from 'redux'//applyMiddleware把中間件應用起來,可以放多個
import createSagaMiddleware from 'redux-saga'
import rootSaga from './sagas'
//使用 `redux-saga` 中間件将 Saga 與 Redux Store 建立連接配接。
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
有多個
saga
同時啟動,需要進行整合,在
saga.js
子產品中引入
rootSaga
//sagas.js
import { delay } from 'redux-saga'
import { put, takeEvery,all } from 'redux-saga/effects'
export function* helloSaga() {
console.log('Hello Sagas!');
}
export function* incrementAsync() {
...
}
function* watchIncrementAsync() {
...
}
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
}
使用:
調用函數(call)
我們從 Generator 裡
yield Effect
以表達 Saga 邏輯。
yield
後如果是一個
promise
,因為
promise
不能進行比較,為了友善測試,在調用函數的時候都寫成
call
的形式。
yield
後的表達式
call(fetch, '/users')
被傳遞給
next
的調用者。
- 先後執行多個任務
import { call } from 'redux-saga/effects'
const users = yield call(fetch, '/users'),
repos = yield call(fetch, '/repos')
- 同步執行多個任務
當我們需要
yield
一個包含 effects 的數組, generator 會被阻塞直到所有的 effects 都執行完畢,或者當一個 effect 被拒絕。一旦其中任何一個任務被拒絕,并行的 Effect 将會被拒絕。在這種情況中,所有其他的 Effect 将被自動取消。
import { call } from 'redux-saga/effects'
const [users, repos] = yield [
call(fetch, '/users'),
call(fetch, '/repos')
]
- 擷取執行最快的任務
在
race
Effect 中。所有參與 race 的任務,除了優勝者(譯注:最先完成的任務),其他任務都會被取消。
- 可用作逾時處理
import { race, call, put } from 'redux-saga/effects'
import { delay } from 'redux-saga'
function* fetchPostsWithTimeout() {
//這裡的 posts和timeout 隻有執行快的那個能取得值
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'})
}
- 自動取消失敗的 Effects
import { race, take, call } from 'redux-saga/effects'
function* backgroundTask() {
while (true) { ... }
}
function* watchStartBackgroundTask() {
while (true) {
yield take('START_BACKGROUND_TASK')
//當 CANCEL_TASK 被發起,race 将自動取消 backgroundTask,并在 backgroundTask 中抛出取消錯誤。
yield race({
task: call(backgroundTask),
cancel: take('CANCEL_TASK')
})
}
}
發起action(put)
put
,這個函數用于建立 dispatch Effect。檢查 yield 後的 Effect,并確定它包含正确的指令。
import { call, put } from 'redux-saga/effects'
//...
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// 建立并 yield 一個 dispatch Effect
//dispatch({ type: 'PRODUCTS_RECEIVED', products })
yield put({ type: 'PRODUCTS_RECEIVED', products })
}
使用 saga 輔助函數
- takeEvery: 允許多個
執行個體同時啟動。在每一個fetchData
到來時派生一個新的任務。action
- takeLatest:得到最新那個請求的響應。 如果已經有一個任務在執行的時候啟動另一個
,那之前的這個任務會被自動取消。fetchData
//sagas.js
import { takeEvery,takeLatest } from 'redux-saga'
function* fetchData(action) { ... }
function* watchFetchData() {
yield* takeEvery('FETCH_REQUESTED', fetchData)
}
function* watchFetchData1() {
yield* takeLatest('FETCH_REQUESTED', fetchData)
}
如果你有多個
Saga
監視不同的
action
,可以用多個内置輔助函數建立不同的觀察者
import { takeEvery } from 'redux-saga/effects'
// FETCH_USERS
function* fetchUsers(action) { ... }
// CREATE_USER
function* createUser(action) { ... }
// 同時使用它們
export default function* rootSaga() {
yield takeEvery('FETCH_USERS', fetchUsers)
yield takeEvery('CREATE_USER', createUser)
}
等待action(take)
- takeEvery
把監聽的動作換為通配符
*
。在每次 action 被比對時一遍又一遍地被調用(無法控制何時被調用),無法控制何時停止監聽。
import { select, takeEvery } 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
take
會暫停
Generator
直到一個比對的
action
被主動拉取。
- 監聽單個
take(‘LOGOUT’)action
- 監聽多個并發的
,隻要捕獲到多個action
action
中的一個,就執行take之後的内容。
take([‘LOGOUT’, ‘LOGIN_ERROR’])
- 監聽所有的
take(’*’)action
import { select, take } 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)
}
}
我們先把上面的代碼
copy
下來
import { takeEvery } from 'redux-saga/effects'
// FETCH_USERS
function* fetchUsers(action) { ... }
// CREATE_USER
function* createUser(action) { ... }
// 同時使用它們
export default function* rootSaga() {
yield takeEvery('FETCH_USERS', fetchUsers)
yield takeEvery('CREATE_USER', createUser)
}
當兩到多個
action
互相之間沒有邏輯關系時,我們可以使用
takeEvery
。
但是,當
action
之間存在邏輯關系後,使用
takeEvery
就會出現問題。比如登陸和登出。
由于
takeEvery
對
action
的分開監管降低了可讀性,程式員必須閱讀多個處理函數的
takeEvery
源代碼并建立起它們之間的邏輯關系。
我們可以用
take
把它改成這樣,這樣兩個代碼
export default function* loginFlow() {
while(true) {
yield take('LOGIN')
// ... perform the login logic
yield take('LOGOUT')
// ... perform the logout logic
}
}
export default function* rootSaga() {
yield* userSaga()
}
優點
-
主動拉取saga
,可以控制監聽的開始和結束action
- 用同步的風格描述控制流,提高了可讀性。
無阻塞調用(fork)和取消調用(cancel)
call
調用時會發生阻塞。 當我們不想錯過
call
下面的
take
等待的
action
,想讓異步調用和等待并行發生時,我們可以用
fork
取代
call
。
當
fork
被調用時,它會在背景啟動 task 并傳回 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'))
}
}
一旦任務被
fork
,可以使用
yield cancel(task)
來中止任務執行。取消正在運作的任務。
cancel
會導緻被
fork
的
task
跳進它的
finally
區塊,我們可以在
finally
區塊中進行清理狀态的操作。
finally
區塊中,可使用
yield cancelled()
來檢查 Generator 是否已經被取消。
import { take, call, put, cancelled } 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})
yield call(Api.storeItem, {token})
return token
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
} finally {
if (yield cancelled()) {
// ... put special cancellation handling code here
}
}
}
⚠️注意:
yield cancel(task)
不會等待被取消的任務完成(即執行其 catch 區塊)。一旦取消,任務通常應盡快完成它的清理邏輯然後傳回。
錯誤處理
- 使用
文法在 Saga 中捕獲錯誤try/catch
import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'
// ...
function* fetchProducts() {
try {
const products = yield call(Api.fetch, '/products')
yield put({ type: 'PRODUCTS_RECEIVED', products })
}
catch(error) {
yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}
}
- 捕捉 Promise 的拒絕操作,并将它們映射到一個錯誤字段對象。
import Api from './path/to/api'
import { call, put } from 'redux-saga/effects'
function fetchProductsApi() {
return Api.fetch('/products')
.then(response => ({ response }))
.catch(error => ({ error }))
}
function* fetchProducts() {
const { response, error } = yield call(fetchProductsApi)
if (response)
yield put({ type: 'PRODUCTS_RECEIVED', products: response })
else
yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}
測試
//saga.spec.js
import { call, put } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// 期望一個 call 指令
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
// 建立一個假的響應對象
const products = {}
// 期望一個 dispatch 指令
assert.deepEqual(
iterator.next(products).value,
put({ type: 'PRODUCTS_RECEIVED', products }),
"fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })"
)
// 建立一個模拟的 error 對象
const error = {}
// 期望一個 dispatch 指令
assert.deepEqual(
iterator.throw(error).value,
put({ type: 'PRODUCTS_REQUEST_FAILED', error }),
"fetchProducts should yield an Effect put({ type: 'PRODUCTS_REQUEST_FAILED', error })"
)
當測試出現分叉的時候,可以調用
clone
方法
import { put, take } from 'redux-saga/effects';
import { cloneableGenerator } from 'redux-saga/utils';
test('doStuffThenChangeColor', assert => {
const gen = cloneableGenerator(doStuffThenChangeColor)();
//前面都是一樣的
gen.next(); // DO_STUFF
gen.next(); // CHOOSE_NUMBER
//判斷奇偶的時候出現了分叉
assert.test('user choose an even number', a => {
const clone = gen.clone();
a.deepEqual(
clone.next(chooseNumber(2)).value,
put(changeUI('red')),
'should change the color to red'
);
a.equal(
clone.next().done,
true,
'it should be done'
);
a.end();
});
assert.test('user choose an odd number', a => {
const clone = gen.clone();
a.deepEqual(
clone.next(chooseNumber(3)).value,
put(changeUI('blue')),
'should change the color to blue'
);
a.equal(
clone.next().done,
true,
'it should be done'
);
a.end();
});
});