技巧

节流(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 task
  while (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 500ms
  yield delay(500)
  ...
}

function* watchInput() {
  // 将取消当前执行的 handleInput task
  yield 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 包含 pastpresentfuture state 的 undo。 甚至有一个 redux-undo library,它建立了一个高阶 reducer 来帮助开发者做那些繁重的工作。

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

使用 redux-saga 的 delayrace 我们可以实现一个简单的、一次性的 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 } = action
  const undoId = `UNDO_ARCHIVE_${threadId}`

  const thread = { id: threadId, archived: true }

  // show undo UI element, and provide a key to communicate
  yield 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-condition
  const { 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 answer
  yield put(actions.hideUndo(undoId))

  if (undo) {
    // revert thread to previous state
    yield put(actions.updateThread({ id: threadId, archived: false }))
  } else if (archive) {
    // make the API call to apply the changes remotely
    yield call(updateThreadApi, thread)
  }
}

function* main() {
  while (true) {
    // wait for an ARCHIVE_THREAD to happen
    const 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 client
    yield spawn(onArchive, action)
  }
}

results matching ""

    No results matching ""