Skip to main content

操作(Actions)

Redux 常见问题:操作(Actions)​

为什么 type 应该是字符串?为什么 action 类型应该是常量?​

与状态一样,可序列化的操作支持 Redux 的几个核心特性,比如时间旅行调试,以及记录和重放操作。如果 type 使用类似 Symbol 的类型或对操作本身使用 instanceof 检查,就会破坏这些功能。字符串是可序列化且自描述的,因此是更好的选择。请注意,如果操作旨在由中间件使用,操作中使用 Symbols、Promises 或其他不可序列化的值也是可以的。操作只需在真正到达 store 并传递给 reducer 时是可序列化的。

出于性能考虑,我们无法可靠地强制所有操作都可序列化,所以 Redux 仅检查每个操作必须是一个普通对象,且 type 是字符串。其余由你自己决定,但你可能会发现保持一切可序列化有助于调试和重现问题。

封装并集中常用代码片段是编程的关键概念。虽然确实可以在任何地方手动创建操作对象并手写每个 type 字符串,但定义可复用的常量会使代码维护更便捷。如果把常量放到单独的文件中,还可以通过工具检查导入语句中的拼写错误,防止意外使用错误的字符串。

如今通常不需要手动编写 action 类型常量。 createSlice 会根据 slice 的 name 和 reducers 字段生成类型字符串(格式为 sliceName/reducerName,例如 'todos/todoAdded')及对应的 action creator,并将 reducer 与这些类型关联起来。派发生成的 action creator,让 TypeScript 帮你捕获拼写错误。

更多信息​

文档

讨论

reducer 和操作之间是否总是一一对应的?​

不是。我们建议编写独立的小型 reducer 函数,每个 reducer 负责特定状态片段的更新。我们称这种模式为“reducer 组合”。一个操作可以被全部、部分或无任何 reducer 处理。这样保持组件与实际数据变动解耦,因为一个操作可能影响状态树的不同部分,组件无需知道这些细节。一些用户会选择更紧密地绑定它们,比如“ducks” 文件结构,但默认情况下绝不存在一一对应关系。如果你发现需要在多个 reducer 中处理同一个操作,应该跳出一一对应的思维模式。

更多信息​

文档

讨论

如何表现 “副作用” 例如 AJAX 请求?为什么需要 “操作创建者”、“thunks” 和 “中间件” 来处理异步行为?​

这是一个复杂且意见多样的话题,关于代码应如何组织以及应采用何种方案各家观点不一。

任何有意义的 Web 应用都需要执行复杂逻辑,通常包括异步工作,如 AJAX 请求。这类代码不再单纯是输入的函数,其与外部世界的交互称为“副作用”。

Redux 受到函数式编程的启发,开箱即用时没有执行副作用的方案。具体来说,reducer 函数必须始终是纯函数,形式为 (state, action) => newState。但 Redux 的中间件可以拦截派发的操作并在周围添加复杂行为,包括副作用。

通常,Redux 建议把副作用代码放进操作创建流程中。尽管这些逻辑可以写在 UI 组件中,但通常更合理抽象成可复用函数,这样同一逻辑可被多处调用,也就是操作创建者函数。

最简单、最常见的方式是使用 Redux Thunk 中间件,以便在 action creator 中编写更复杂的异步逻辑。configureStore 默认会添加它,而 createAsyncThunk 会为基于 Promise 的 thunk 生成 pending、fulfilled 和 rejected action。对于数据获取和缓存,RTK Query 会处理完整的请求生命周期,你无需自行编写这些逻辑。listener middleware 适用于“某个 action 派发后运行副作用”的场景。此外,Redux Saga 使用 generator 编写更接近同步形式的代码,也能在 Redux 应用中充当“后台线程”;社区中还有其他用于管理副作用的库。

更多信息​

文档

文章

讨论

应该使用哪种异步中间件?如何在 thunks、sagas、observables 或其他方案间做选择?​

目前有许多异步或副作用中间件可选,最常用的包括 redux-thunk、Redux Toolkit 的 listener middleware、redux-saga 和 redux-observable。它们各有优缺点,适用场景也不同。

通用建议:

  • 如果逻辑用于从服务器获取并缓存数据,请使用 RTK Query,无需自行实现这部分逻辑。
  • Thunk 适用于复杂的同步逻辑(尤其是需要访问整个 Redux store 状态的代码)以及简单的异步逻辑(例如基本的 AJAX 请求)。使用 async/await 后,thunk 也可以处理一些更复杂的 Promise 逻辑。
  • Listener middleware 适用于_响应_已派发 action 或状态变化的逻辑,例如“用户登录后开始轮询”或“添加待办后持久化列表”。它也可以取消或防抖任务。无需使用 generator 函数,就能覆盖 saga 的许多常见用途。
  • Saga 适用于复杂异步工作流,以及 listener middleware 无法处理的、彼此解耦的“后台线程”式行为。使用它需要熟悉 generator 函数和 redux-saga 的“effects”操作符。
  • Observable 解决的问题与 saga 类似,但依赖 RxJS 实现异步行为,因此需要熟悉 RxJS API。

我们建议大多数 Redux 用户使用 RTK Query 获取数据,使用 thunk 处理其他逻辑;需要响应 action 时再加入 listener middleware,只有应用确实需要处理更复杂的异步工作流时,才考虑 saga 或 observable。更详细的比较请参阅风格指南中的“使用 thunk 和 listener 处理其他异步逻辑”以及副作用处理方案。

因为 sagas 和 observables 适用场景相同,一个应用一般选其一,而不会两者兼用。但注意,使用 thunks 以及 sagas 或 observables 其一是完全可以共存的,因为它们解决的是不同的问题。

文章

讨论

我应该在一个操作创建者中连续派发多个操作吗?​

对于操作的结构没有硬性规定。像 Redux Thunk 这样的异步中间件当然支持连续派发多个相关但不同的操作,派发表示 AJAX 请求进度的操作,根据状态条件派发操作,甚至派发某个操作后立即检查更新的状态。

一般来说,考虑这些操作是相关但独立的,还是应该合并为一个操作。根据你的具体情况做决定,但要权衡 reducer 的可读性和操作日志的可读性。例如,一个包含整个新状态树的操作会让 reducer 变成一行代码,但缺点是你失去了变化原因的历史记录,调试会非常困难。反之,如果你通过循环发送多个细粒度操作,那可能意味着你应该引入一种新的操作类型,并通过不同方式处理。

如果你关心性能,尽量避免在同一时刻连续同步派发多次操作。有许多插件和方案可以对 dispatch 操作进行合并批处理。

更多信息​

文档

文章

讨论