技巧

节流(Throttling)

你可以通过在监听的 Saga 里调用一个 delay 函数,针对一系列发起的 action 进行节流。 举个例子,假设用户在文本框输入文字的时候,UI 触发了一个 INPUT_CHANGED action:

  1. import { throttle } from 'redux-saga/effects'
  2. function* handleInput(input) {
  3. // ...
  4. }
  5. function* watchInput() {
  6. yield throttle(500, 'INPUT_CHANGED', handleInput)
  7. }

通过使用 throttle helper,watchInput 不会在 500ms 启动一个新的 handleInput task,但在相同时间內,它仍然接受最新的 INPUT_CHANGED 到底层的 buffer,所以它会忽略所有的 INPUT_CHANGED action。 这确保了 Saga 在 500ms 这段时间,最多接受一个 INPUT_CHANGED action,并且可以继续处理 trailing action。

防抖动(Debouncing)

为了对 action 队列进行防抖动,可以在被 fork 的任务里放一个内置的 delay

  1. import { call, cancel, fork, take, delay } from 'redux-saga/effects'
  2. function* handleInput(input) {
  3. // 500ms 防抖动
  4. yield delay(500)
  5. ...
  6. }
  7. function* watchInput() {
  8. let task
  9. while (true) {
  10. const { input } = yield take('INPUT_CHANGED')
  11. if (task) {
  12. yield cancel(task)
  13. }
  14. task = yield fork(handleInput, input)
  15. }
  16. }

在上面的示例中,handleInput 在执行之前等待了 500ms。如果用户在此期间输入了更多文字,我们将收到更多的 INPUT_CHANGED action。 并且由于 handleInput 仍然会被 delay 阻塞,所以在执行自己的逻辑之前它会被 watchInput 取消。

上面的例子可以使用 redux-saga 的 takeLatest helper 重写:

  1. import { call, takeLatest, delay } from 'redux-saga/effects'
  2. function* handleInput({ input }) {
  3. // debounce by 500ms
  4. yield delay(500)
  5. ...
  6. }
  7. function* watchInput() {
  8. // 将取消当前执行的 handleInput task
  9. yield takeLatest('INPUT_CHANGED', handleInput);
  10. }

ajax 重试(Retrying XHR calls)

为了重试指定次数的 XHR 调用,使用一个 for 循环和一个 delay:

  1. import { call, put, take, delay } from 'redux-saga/effects'
  2. function* updateApi(data) {
  3. for(let i = 0; i < 5; i++) {
  4. try {
  5. const apiResponse = yield call(apiRequest, { data });
  6. return apiResponse;
  7. } catch(err) {
  8. if(i < 4) {
  9. yield delay(2000);
  10. }
  11. }
  12. }
  13. // 重试 5 次后失败
  14. throw new Error('API request failed');
  15. }
  16. export default function* updateResource() {
  17. while (true) {
  18. const { data } = yield take('UPDATE_START');
  19. try {
  20. const apiResponse = yield call(updateApi, data);
  21. yield put({
  22. type: 'UPDATE_SUCCESS',
  23. payload: apiResponse.body,
  24. });
  25. } catch (error) {
  26. yield put({
  27. type: 'UPDATE_ERROR',
  28. error
  29. });
  30. }
  31. }
  32. }

在上面的例子中,apiRequest 将重试 5 次,每次延迟 2 秒。在第 5 次失败后,将会通过父级 saga 抛出一个异常,这将会 dispatch UPDATE_ERROR action。

如果你想要无限重试,可以把 for 循环替换成 while (true)。你也可以使用 takeLatest 来替代 take,这样就只会重试最后一次的请求。在错误处理中加入一个 UPDATE_RETRY action,我们就可以通知使用者更新没有成功,但是它会重试。

  1. import { delay } from 'redux-saga/effects'
  2. function* updateApi(data) {
  3. while (true) {
  4. try {
  5. const apiResponse = yield call(apiRequest, { data });
  6. return apiResponse;
  7. } catch(error) {
  8. yield put({
  9. type: 'UPDATE_RETRY',
  10. error
  11. })
  12. yield delay(2000);
  13. }
  14. }
  15. }
  16. function* updateResource({ data }) {
  17. const apiResponse = yield call(updateApi, data);
  18. yield put({
  19. type: 'UPDATE_SUCCESS',
  20. payload: apiResponse.body,
  21. });
  22. }
  23. export function* watchUpdateResource() {
  24. yield takeLatest('UPDATE_START', updateResource);
  25. }

撤销(Undo)

Undo 通过允许 action 顺利进行来尊重使用者,在假设使用者不知道他们在做什么之前。GoodUI redux 文档 描述了一个 稳定的方式来实现一个基于修改 reducer 包含 pastpresentfuture state 的 undo。 甚至有一个 redux-undo library,它建立了一个高阶 reducer 来帮助开发者做那些繁重的工作。

然而,这个方法附带了一些开销,因为它的 store 引用了应用之前的 state(s)。

使用 redux-saga 的 delayrace 我们可以实现一个简单的、一次性的 undo,不需要 enhance 我们的 reducer 或 store 先前的 state。

  1. import { take, put, call, spawn, race, delay } from 'redux-saga/effects'
  2. import { updateThreadApi, actions } from 'somewhere'
  3. function* onArchive(action) {
  4. const { threadId } = action
  5. const undoId = `UNDO_ARCHIVE_${threadId}`
  6. const thread = { id: threadId, archived: true }
  7. // show undo UI element, and provide a key to communicate
  8. yield put(actions.showUndo(undoId))
  9. // optimistically mark the thread as `archived`
  10. yield put(actions.updateThread(thread))
  11. // allow the user 5 seconds to perform undo.
  12. // after 5 seconds, 'archive' will be the winner of the race-condition
  13. const { undo, archive } = yield race({
  14. undo: take(action => action.type === 'UNDO' && action.undoId === undoId),
  15. archive: delay(5000)
  16. })
  17. // hide undo UI element, the race condition has an answer
  18. yield put(actions.hideUndo(undoId))
  19. if (undo) {
  20. // revert thread to previous state
  21. yield put(actions.updateThread({ id: threadId, archived: false }))
  22. } else if (archive) {
  23. // make the API call to apply the changes remotely
  24. yield call(updateThreadApi, thread)
  25. }
  26. }
  27. function* main() {
  28. while (true) {
  29. // wait for an ARCHIVE_THREAD to happen
  30. const action = yield take('ARCHIVE_THREAD')
  31. // use spawn to execute onArchive in a non-blocking fashion, which also
  32. // prevents cancellation when main saga gets cancelled.
  33. // This helps us in keeping state in sync between server and client
  34. yield spawn(onArchive, action)
  35. }
  36. }