Saga 优势
保持 action 的纯粹性
action 应该是无副作用的,saga 通过 effects 的方式保持 action 的纯粹性
强大的异步处理
saga 使用 generator 模式工作,并将 effects 分为不同类型交给中间件分别处理,对于复杂异步处理十分方便:
阻塞与无阻塞
- fork 是无阻塞型
- call 是阻塞型
effects 并发
- takeEvery:每次都执行 task,不管之前的 task 是否结束
- takeLatest:只执行最新的一次 task,如果之前的任务还没结束,则自动取消。
可控的 action flow
- 同时执行多个 effects:
yield [ call(fetch, '/users'), call(fetch, '/repos')] - cancel: 取消一个 task
import { SagaCancellationException } from 'redux-saga'
import { take, put, call, fork, cancel } from 'redux-saga/effects'
import actions from 'somewhere'
import { someApi, delay } from 'somewhere'
function* bgSync() {
try {
while(true) {
yield put({type: 'startQuery'})
const result = yield call(someApi)
yield put({type: 'updateState', result})
yield call(delay, 5000)
}
} catch(error) {
// 或直接使用 `isCancelError(error)`
if(error instanceof SagaCancellationException)
yield put(actions.requestFailure('Sync cancelled!'))
}
}
function* main() {
while( yield take(START_BACKGROUND_SYNC) ) {
// 启动后台任务
const bgSyncTask = yield fork(bgSync)
// 等待用户的停止操作
yield take(STOP_BACKGROUND_SYNC)
// 用户点击了停止,取消后台任务
// 将抛出一个 SagaCancellationException 错误至被 fork 的 bgSync 任务
yield cancel(bgSyncTask)
}
}
- take: 暂停 generator ,直到匹配到特定的 action (可以理解为监听一个 action),让我们能在一个集中的地方更好地去描述一个非常规的流程。例如,我们的登陆登出流程,只有存在登陆的情况下,才有可能有登出,因此我们可以把登陆和登出放在同一个 saga task 中处理:
import { isCancelError } from 'redux-saga'
import { take, call, put } from 'redux-saga/effects'
function* authorize(user, password) {
try {
const token = yield call(Api.authorize, user, password)
yield put({type: 'LOGIN_SUCCESS', token})
} catch(error) {
if(!isCancelError(error))
yield put({type: 'LOGIN_ERROR', error})
}
}
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'))
}
}
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
function* handleInput(input) {
// 500ms 防抖动
yield call(delay, 500)
...其他逻辑巴拉巴拉
}
function* watchInput() {
let task
while(true) {
const { input } = yield take('INPUT_CHANGED')
if(task)
yield cancel(task)
task = yield fork(handleInput, input)
}
}
- race:多个 effects 竞争,只取最先成功的那个,其他自动取消
import { race, take, put } from 'redux-saga/effects'
function* backgroundTask() {
// 轮询
while (true) {
yield call(delay, 1000);
yield call(api, params);
}
}
function* watchStartBackgroundTask() {
while(true) {
yield take('START_BACKGROUND_TASK')
yield race({
task: call(backgroundTask),
cancel: take('CANCEL_TASK')
})
}
}
可测试
saga 将 effects 分多种类型,每一个 effects 都是描述了具体执行信息的对象,比如说我们常用的 put 和 call,就是两种不同的 effects 。put 是发起一个 action 到 store,而 call 则调用给定的函数。
每次我们 yield 一个 put 或者 call,返回的结果都是纯文本的当前 effects 信息。因此,这种将 E、effect 创建和 effect 执行之间分开的做法,使得可以测试 effects。
e.g.
// saga.js
*query() {
const data = yield call(query);
yield put({type: 'updateState', data});
}
// test.js
const iterator = query();
assert.deepEqual(
iterator.next().value,
call(query),
"query should yield an Effect call(query)"
)
assert.deepEqual(
iterator.next().value,
put({type: 'updateState', data}),
"query should yield an Effect put({type: 'updateState', data})"
)
assert.deepEqual(
iterator.next(),
{ done: true, value: undefined },
'query must be done'
)
