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()
])
}
我们从 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'})
}
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')
})
}
}
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 })
}
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 被匹配时一遍又一遍地被调用(无法控制何时被调用),无法控制何时停止监听。
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
会暂停 Generator
直到一个匹配的 action
被主动拉取。
监听单个action
take(‘LOGOUT’)
监听多个并发的action
,只要捕获到多个action
中的一个,就执行take之后的内容。
take([‘LOGOUT’, ‘LOGIN_ERROR’])
监听所有的action
take(’*’)
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
,可以控制监听的开始和结束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 区块)。一旦取消,任务通常应尽快完成它的清理逻辑然后返回。
try/catch
语法在 Saga 中捕获错误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 })
}
}
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();
});
});
为了运行上面的测试代码,我们需要运行:
$ yarn test