技巧
节流(Throttling)
你可以通过在监听的 Saga 里调用一个 delay 函数,针对一系列发起的 action 进行节流。
举个例子,假设用户在文本框输入文字的时候,UI 触发了一个 INPUT_CHANGED action:
import { throttle } from 'redux-saga/effects'function* handleInput(input) {// ...}function* watchInput() {yield throttle(500, 'INPUT_CHANGED', handleInput)}
通过使用 throttle helper,watchInput 不会在 500ms 启动一个新的 handleInput task,但在相同时间內,它仍然接受最新的 INPUT_CHANGED 到底层的 buffer,所以它会忽略所有的 INPUT_CHANGED action。
这确保了 Saga 在 500ms 这段时间,最多接受一个 INPUT_CHANGED action,并且可以继续处理 trailing action。
防抖动(Debouncing)
为了对 action 队列进行防抖动,可以在被 fork 的任务里放一个内置的 delay。
import { call, cancel, fork, take, delay } from 'redux-saga/effects'function* handleInput(input) {// 500ms 防抖动yield delay(500)...}function* watchInput() {let taskwhile (true) {const { input } = yield take('INPUT_CHANGED')if (task) {yield cancel(task)}task = yield fork(handleInput, input)}}
在上面的示例中,handleInput 在执行之前等待了 500ms。如果用户在此期间输入了更多文字,我们将收到更多的 INPUT_CHANGED action。
并且由于 handleInput 仍然会被 delay 阻塞,所以在执行自己的逻辑之前它会被 watchInput 取消。
上面的例子可以使用 redux-saga 的 takeLatest helper 重写:
import { call, takeLatest, delay } from 'redux-saga/effects'function* handleInput({ input }) {// debounce by 500msyield delay(500)...}function* watchInput() {// 将取消当前执行的 handleInput taskyield takeLatest('INPUT_CHANGED', handleInput);}
ajax 重试(Retrying XHR calls)
为了重试指定次数的 XHR 调用,使用一个 for 循环和一个 delay:
import { call, put, take, delay } from 'redux-saga/effects'function* updateApi(data) {for(let i = 0; i < 5; i++) {try {const apiResponse = yield call(apiRequest, { data });return apiResponse;} catch(err) {if(i < 4) {yield delay(2000);}}}// 重试 5 次后失败throw new Error('API request failed');}export default function* updateResource() {while (true) {const { data } = yield take('UPDATE_START');try {const apiResponse = yield call(updateApi, data);yield put({type: 'UPDATE_SUCCESS',payload: apiResponse.body,});} catch (error) {yield put({type: 'UPDATE_ERROR',error});}}}
在上面的例子中,apiRequest 将重试 5 次,每次延迟 2 秒。在第 5 次失败后,将会通过父级 saga 抛出一个异常,这将会 dispatch UPDATE_ERROR action。
如果你想要无限重试,可以把 for 循环替换成 while (true)。你也可以使用 takeLatest 来替代 take,这样就只会重试最后一次的请求。在错误处理中加入一个 UPDATE_RETRY action,我们就可以通知使用者更新没有成功,但是它会重试。
import { delay } from 'redux-saga/effects'function* updateApi(data) {while (true) {try {const apiResponse = yield call(apiRequest, { data });return apiResponse;} catch(error) {yield put({type: 'UPDATE_RETRY',error})yield delay(2000);}}}function* updateResource({ data }) {const apiResponse = yield call(updateApi, data);yield put({type: 'UPDATE_SUCCESS',payload: apiResponse.body,});}export function* watchUpdateResource() {yield takeLatest('UPDATE_START', updateResource);}
撤销(Undo)
Undo 通过允许 action 顺利进行来尊重使用者,在假设使用者不知道他们在做什么之前。GoodUI
redux 文档 描述了一个
稳定的方式来实现一个基于修改 reducer 包含 past、present、future state 的 undo。
甚至有一个 redux-undo library,它建立了一个高阶 reducer 来帮助开发者做那些繁重的工作。
然而,这个方法附带了一些开销,因为它的 store 引用了应用之前的 state(s)。
使用 redux-saga 的 delay 和 race 我们可以实现一个简单的、一次性的 undo,不需要 enhance 我们的 reducer 或 store 先前的 state。
import { take, put, call, spawn, race, delay } from 'redux-saga/effects'import { updateThreadApi, actions } from 'somewhere'function* onArchive(action) {const { threadId } = actionconst undoId = `UNDO_ARCHIVE_${threadId}`const thread = { id: threadId, archived: true }// show undo UI element, and provide a key to communicateyield put(actions.showUndo(undoId))// optimistically mark the thread as `archived`yield put(actions.updateThread(thread))// allow the user 5 seconds to perform undo.// after 5 seconds, 'archive' will be the winner of the race-conditionconst { undo, archive } = yield race({undo: take(action => action.type === 'UNDO' && action.undoId === undoId),archive: delay(5000)})// hide undo UI element, the race condition has an answeryield put(actions.hideUndo(undoId))if (undo) {// revert thread to previous stateyield put(actions.updateThread({ id: threadId, archived: false }))} else if (archive) {// make the API call to apply the changes remotelyyield call(updateThreadApi, thread)}}function* main() {while (true) {// wait for an ARCHIVE_THREAD to happenconst action = yield take('ARCHIVE_THREAD')// use spawn to execute onArchive in a non-blocking fashion, which also// prevents cancellation when main saga gets cancelled.// This helps us in keeping state in sync between server and clientyield spawn(onArchive, action)}}
