跳至主要内容

食谱

节流

您可以使用方便的内置 throttle 辅助函数来节流一系列分派的 action。例如,假设 UI 在用户在文本字段中键入时触发 INPUT_CHANGED action。

import { throttle } from 'redux-saga/effects'

function* handleInput(input) {
// ...
}

function* watchInput() {
yield throttle(500, 'INPUT_CHANGED', handleInput)
}

使用这个助手,watchInput 不会在 500 毫秒内启动新的 handleInput 任务,但同时它仍然会接受最新的 INPUT_CHANGED 动作到其底层的 buffer 中,因此它会错过期间发生的任何 INPUT_CHANGED 动作。这确保了 Saga 在每 500 毫秒内最多处理一个 INPUT_CHANGED 动作,并且仍然能够处理尾随动作。

防抖动

从 redux-saga@v1 开始,debounce 是内置效果。

让我们考虑如何将该效果实现为其他基本效果的组合。

为了对序列进行防抖动,将内置的 delay 助手放在分叉的任务中


import { call, cancel, fork, take, delay } from 'redux-saga/effects'

function* handleInput(input) {
// debounce by 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 在执行其逻辑之前等待 500 毫秒。如果用户在此期间输入内容,我们将获得更多 INPUT_CHANGED 动作。由于 handleInput 仍然被阻塞在 delay 调用中,它将在开始执行其逻辑之前被 watchInput 取消。

上面的示例可以使用 redux-saga 的 takeLatest 助手重写


import { call, takeLatest, delay } from 'redux-saga/effects'

function* handleInput({ input }) {
// debounce by 500ms
yield delay(500)
...
}

function* watchInput() {
// will cancel current running handleInput task
yield takeLatest('INPUT_CHANGED', handleInput);
}

重试 XHR 调用

从 redux-saga@v1 开始,retry 是内置效果。

让我们考虑如何将该效果实现为其他基本效果的组合。

要重试特定次数的 XHR 调用,请使用带有延迟的 for 循环

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)
}
}
}
// attempts failed after 5 attempts
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 捕获,它将调度 UPDATE_ERROR 动作。

如果你想要无限次重试,那么 for 循环可以用 while (true) 替换。此外,可以使用 takeLatest 代替 take,这样只有最后一个请求会被重试。通过在错误处理中添加 UPDATE_RETRY 动作,我们可以通知用户更新不成功,但会重试。

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)
}

撤销

撤销功能体现了对用户的尊重,它首先让操作顺利进行,然后再假设用户不知道自己在做什么(链接)。Redux 文档 描述了一种强大的撤销实现方式,它通过修改 reducer 来包含 pastpresentfuture 状态。甚至有一个库 redux-undo 可以创建更高阶的 reducer,为开发者完成大部分繁重的工作。

然而,这种方法会带来开销,因为它存储了应用程序先前状态的引用。

使用 redux-saga 的 delayrace,我们可以实现基本的、一次性的撤销功能,而无需增强 reducer 或存储先前状态。

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)
}
}

批量操作

redux 不支持一次性分派多个操作并只调用 reducer 一次。这会影响性能,而且需要按顺序分派多个操作的代码体验也不佳。

因此,我们转向第三方库 redux-batched-actions。这是一个简单的 reducer 和 action,允许最终开发者分派多个操作,并且只调用一次 reducer。

如果你的代码库需要同时分派多个操作,我们建议使用这种方法。

import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware, { stdChannel } from 'redux-saga';
import { enableBatching, BATCH } from 'redux-batched-actions';

// your root reducer
import { rootReducer } from './reducer';
// your root saga
import { rootSaga } from './saga';

const channel = stdChannel();
const rawPut = channel.put;
channel.put = (action: ActionWithPayload<any>) => {
if (action.type === BATCH) {
action.payload.forEach(rawPut);
return;
}
rawPut(action);
};
const sagaMiddleware = createSagaMiddleware({ channel });

const reducer = enableBatching(rootReducer);
// https://toolkit.redux.js.cn/api/configureStore
const store = configureStore({
reducer: rootReducer,
middleware: [sagaMiddleware],
});
sagaMiddleware.run(rootSaga);