Redux Actions 实战指南type 字符串约定、Reducer 组合与异步副作用处理【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux导读本文聚焦 Redux 中 Action 的核心设计问题为什么type必须是字符串、为什么要用常量定义 action 类型、reducer 与 action 是否一一对应、AJAX 等副作用应如何组织以及 thunk / saga / observable 等异步中间件如何选型。文中所有结论均结合本仓库redux 核心库的源码实现与测试用例进行印证读完你不仅能理解设计哲学还能直接写出规范、可维护、可调试的 action 代码。1. 为什么type必须是字符串为什么要用常量定义 action 类型1.1 可序列化是 Redux 一切高级特性的前提Redux 的若干标志性能力——时间旅行调试time travel debugging、动作录制与回放recording and replaying actions——都建立在 action 可序列化的基础之上。action 需要能在不同进程、不同时刻之间传递和还原因此用Symbol作为type值、或用instanceof判断 action 类型都会破坏可序列化性字符串天然可序列化且自描述性强看一眼ADD_TODO就知道它要做什么所以是最佳选择。仓库源码中TypeScript 类型定义明确表达了这一约定。在 src/types/actions.ts 中/** * An *action* is a plain object that represents an intention to change the * state. Actions are the only way to get data into the store. ... * Actions must have a type field ... These must be strings, as strings are serializable. */ export type ActionT extends string string { type: T }同时该文件还定义了UnknownAction带索引签名、允许附加任意属性主要用于Reducer类型和已废弃的AnyAction后者允许任意额外属性。1.2 允许非可序列化值的例外仅限中间件内部需要注意文档明确说明如果 action 是供中间件middleware使用的那么在 action 中使用Symbol、Promise或其他不可序列化值是允许的。action 只需要在真正到达 store、被传给 reducer 之前保持可序列化即可。也就是说不可序列化的值可以出现在中间件层但不能进入 reducer 的输入。1.3 Redux 到底强制检查什么出于性能考虑Redux 无法可靠地递归校验整个 action 的可序列化性因此运行时只做两项检查action 必须是纯对象plain objecttype必须是字符串。这两项检查都能在源码中直接看到。在 src/utils/isAction.ts 中isAction函数组合了两个判断import isPlainObject from ./isPlainObject export default function isAction(action: unknown): action is Actionstring { return ( isPlainObject(action) type in action typeof (action as Recordtype, unknown).type string ) }而isPlainObject见 src/utils/isPlainObject.ts通过沿原型链向上遍历的方式判断对象是否为纯对象。更重要的是dispatch执行时的强制校验见 src/createStore.tsfunction dispatch(action: A) { if (!isPlainObject(action)) { throw new Error( Actions must be plain objects. Instead, the actual type was: ${kindOf(action)}. ... ) } if (typeof action.type undefined) { throw new Error( Actions may not have an undefined type property. You may have misspelled an action type string constant. ) } if (typeof action.type ! string) { throw new Error( Action type property must be a string. ... ) } // ... }三个错误分支分别对应非纯对象例如直接 dispatch 了一个函数此时会提示你可能需要redux-thunk中间件、type为undefined提示可能拼错了 type 常量、type非字符串。这是理解Redux 只检查 action 是纯对象且type是字符串的最直接证据。1.4 用常量集中管理 action type封装与集中管理常用代码是编程的核心思想。虽然可以到处手写 action 对象和字符串字面量但把type定义为常量会显著降低维护成本把常量放在单独文件中借助eslint-plugin-import等工具校验import语句的拼写错误从根上杜绝手写错字符串这类 bug常量天然支持 IDE 自动补全、跳转定义与全局重命名。仓库测试代码 test/helpers/actionTypes.ts 给出了标准写法export const ADD_TODO ADD_TODO export const DISPATCH_IN_MIDDLE DISPATCH_IN_MIDDLE export const GET_STATE_IN_MIDDLE GET_STATE_IN_MIDDLE // ... export const UNKNOWN_ACTION UNKNOWN_ACTION实际示例 examples/todos/src/actions/index.js 展示了 action creator 与常量配合的典型用法let nextTodoId 0 export const addTodo text ({ type: ADD_TODO, id: nextTodoId, text }) export const setVisibilityFilter filter ({ type: SET_VISIBILITY_FILTER, filter }) export const toggleTodo id ({ type: TOGGLE_TODO, id })1.5 补充Redux 保留的私有 action 类型值得注意Redux 内部也遵循这一约定但使用redux/前缀的私有字符串类型见 src/utils/actionTypes.tsconst ActionTypes { INIT: redux/INIT${randomString()}, REPLACE: redux/REPLACE${randomString()}, PROBE_UNKNOWN_ACTION: () redux/PROBE_UNKNOWN_ACTION${randomString()} }这些类型由 Redux 保留用于 store 初始化createStore创建时会 dispatch 一个INITaction 让所有 reducer 返回初始 state见 src/createStore.ts、replaceReducer等内部流程开发者不应在自己的代码中直接引用。2. reducer 与 action 之间是多对多不是一对一2.1 reducer 组合模式答案是否定的不存在 reducer 与 action 的一一对应关系。Redux 官方建议编写相互独立的小型 reducer 函数每个函数只负责更新 state 的特定切片这一模式称为reducer 组合reducer composition。给定一个 action可能被所有 reducer 处理部分 reducer 处理没有任何 reducer 处理例如未匹配到任何分支时 reducer 直接返回当前 state。2.2 这样设计的好处这种松耦合带来两个直接收益组件与数据变化解耦一个 action 可能同时影响 state 树的多个不同部分例如用户登录同时更新user、permissions、ui多个切片组件无需感知这些细节自由扩展任何时候想让多个 reducer 响应同一个 action直接在每个 reducer 的switch分支中处理即可无需改动 action 定义。当然也有开发者倾向于更紧密地绑定例如 ducks 文件结构将 action types、action creators 与 reducer 放在同一模块但这只是个人偏好并非 Redux 的默认约定。相关深入资料见 docs/usage/structuring-reducers/StructuringReducers.md 与 docs/tutorials/fundamentals/part-3-state-actions-reducers.md。3. AJAX 等副作用怎么放action creator、thunk 与 middleware 的定位3.1 问题本质reducer 必须保持纯函数任何有意义的 Web 应用都需要执行复杂逻辑尤其是 AJAX 请求这类异步工作。这类代码不再是输入的函数与外部世界的交互被称为副作用side effects。Redux 深受函数式编程启发开箱即用下没有执行副作用的位置。特别是 reducer 必须始终是(state, action) newState的纯函数。因此需要一套机制把副作用接进来。3.2 核心思路副作用属于动作创建过程Redux 的总体建议是带副作用的代码应属于 action 创建过程。这段逻辑虽然可以写在 UI 组件内部但更合理的做法是抽取为可复用的函数——即action creator以便从多个地方调用同一逻辑。action creator 的定义可参考 src/types/actions.ts/** * An *action creator* is, quite simply, a function that creates an action. ... * Calling an action creator only produces an action, but does not dispatch it. * You need to call the stores dispatch function to actually cause the mutation. * ... * If an action creator needs to read the current state, perform an API call, * or cause a side effect, like a routing transition, it should return an * async action instead of an action. */ export interface ActionCreatorA, P extends any[] any[] { (...args: P): A }注意这段注释的关键信息调用 action creator 只产生 action并不会 dispatch只有调用 store 的dispatch才会真正触发状态变更。需要读取当前 state、执行 API 调用或产生路由跳转等副作用时action creator 应返回异步 action如 thunk 函数而非普通 action。3.3 中间件是副作用的接入点纯函数式的 Redux 通过middleware拦截被 dispatch 的 action并为其包裹额外复杂行为包括副作用。中间件的类型定义见 src/types/middleware.tsexport interface Middleware... { (api: MiddlewareAPID, S): (next: (action: unknown) unknown) (action: unknown) unknown }其运行时装配逻辑在 src/applyMiddleware.ts 中applyMiddleware是一个 store enhancer把多个中间件通过compose串成新的dispatchconst middlewareAPI: MiddlewareAPI { getState: store.getState, dispatch: (action, ...args) dispatch(action, ...args) } const chain middlewares.map(middleware middleware(middlewareAPI)) dispatch composetypeof dispatch(...chain)(store.dispatch)每个中间件都拿到dispatch与getState作为参数从而可以在 dispatch 前后做文章——这正是 thunk 等异步方案能工作的基础。3.4 主流副作用方案一览文档归纳了三种最常见的方案方案核心机制适用场景学习成本Redux Thunkaction creator 返回函数由中间件执行复杂同步逻辑需要访问整个 store state、简单异步逻辑基本 AJAX 调用低Redux Saga用 generator 写同步风格代码可监听 dispatch 的 action复杂异步逻辑、解耦的后台线程/守护进程行为需熟悉 generator 与 saga 的 effects 操作符Redux Loop反转流程reducer 声明副作用由框架单独执行在响应 state 变化时声明副作用中3.5 仓库中的 thunk 风格示例本仓库测试辅助代码 test/helpers/actionCreators.ts 给出了教科书式的 thunk action creatorexport function addTodoAsync(text: string) { return (dispatch: Dispatch): Promisevoid new Promise(resolve setImmediate(() { dispatch(addTodo(text)) resolve() }) ) } export function addTodoIfEmpty(text: string) { return (dispatch: Dispatch, getState: () any) { if (!getState().length) { dispatch(addTodo(text)) } } }addTodoAsync返回一个接收dispatch的函数异步完成后调用dispatch(addTodo(text))这是表示 AJAX 请求进展的基本形态addTodoIfEmpty通过闭包拿到getState实现基于当前 state 条件性 dispatch——这正是文档所说的 thunk 擅长需要访问整个 Redux store state的场景。与redux-thunk配套的 dispatch 入口约束也体现在 src/createStore.ts 的注释中基础实现只支持纯对象 action若想 dispatch Promise、Observable、thunk 等需要用对应中间件包裹 store即使中间件最终也会通过dispatch发出纯对象 action。4. 异步中间件怎么选thunk / saga / observable4.1 通用经验法则文档给出的选型建议非常明确Thunks最适合复杂同步逻辑尤其是需要访问整个 store state 的代码和简单异步逻辑如基本 AJAX 调用。配合async/await也可以合理处理一些较复杂的基于 Promise 的逻辑Sagas最适合复杂异步逻辑和去耦的后台线程式行为尤其是需要监听 dispatch 的 action时这是 thunk 做不到的。代价是需要熟悉 generator 函数和redux-saga的 effects 操作符Observablesredux-observable解决与 sagas 相同的问题但依赖 RxJS 实现异步需要熟悉 RxJS API。4.2 组合使用策略起步建议大多数 Redux 用户应从 thunks 开始当应用确实需要处理更复杂的异步逻辑时再按需引入 sagas 或 observablessaga 与 observable 二选一两者用例重叠一个应用通常只用其中一种thunk 可与 saga/observable 共存完全可以在使用 thunk 的同时使用 saga 或 observable因为它们解决的是不同层面的问题thunk 处理单点逻辑saga/observable 处理全局流程编排。4.3 与核心库的关系需要说明的是redux-thunk、redux-saga、redux-observable均为独立于本仓库的第三方库本文仅转述 FAQ 文档给出的选型建议。在本仓库范围内可验证的是核心库通过applyMiddleware提供了统一的中间件接入机制见 src/applyMiddleware.ts 与 docs/api/applyMiddleware.md任何符合Middleware接口的中间件都可以被组合安装而 docs/tutorials/fundamentals/part-6-async-logic.md 与 docs/tutorials/fundamentals/part-4-store.md 分别讲解了异步逻辑与中间件接入的完整流程。5. 一个 action creator 里能否连续 dispatch 多个 action5.1 没有硬性规则但要有取舍文档明确指出Redux 对 action 的粒度没有硬性规定。借助 Redux Thunk 等异步中间件可以实现连续 dispatch 多个相互关联但独立的 actiondispatch 一系列 action 来表达一个 AJAX 请求的进度如FETCH_REQUEST→FETCH_SUCCESS/FETCH_FAILURE基于当前 state 条件性 dispatchdispatch 后立即检查更新后的 state。5.2 平衡reducer 可读性与action 日志可读性核心权衡点是要问这些 action 是相关但独立还是应该合并成一个 action。若一个 action 直接携带整个新 state 树reducer 会变成一行代码但代价是 action 日志中丢失了为什么发生这些变化的历史信息调试变得极其困难反之若在循环中不断发出颗粒度极细的 action则是一个信号你可能需要引入一个由不同方式处理的新 action 类型。5.3 性能注意事项在关心性能的地方应避免连续同步多次 dispatch。目前社区有多种批量合并batchdispatch 的插件与方案。更详细的讨论见 docs/faq/Performance.md 中如何减少 store 更新事件数量一节。5.4 实用辅助bindActionCreators如果组件希望直接调用绑定后的 action creator 而不显式写store.dispatch核心库提供了bindActionCreators见 src/bindActionCreators.ts。它的实现非常直观把每个 action creator 包装成调用 creator 后立即dispatch其结果的函数function bindActionCreator(actionCreator, dispatch) { return function (this: any, ...args: any[]) { return dispatch(actionCreator.apply(this, args)) } }这也从源码层面印证了第 3 节的核心论断action creator 本身不负责 dispatchdispatch 永远是显式调用绑定只是省去书写的便利层。6. 小结围绕 Actions 的 FAQ可以归纳出四条经得起源码检验的实践准则type用字符串常量这是可序列化调试时间旅行、录制回放的前提createStore的dispatch会强制校验纯对象 字符串 type见 src/createStore.tsreducer 与 action 多对多通过 reducer 组合让不同切片各取所需组件无需关心 action 影响范围副作用交给 action creator 中间件reducer 保持纯函数异步逻辑通过 thunk 等中间件在 dispatch 链路中执行异步选型按复杂度递进thunk 起步复杂流程再引入 saga 或 observable二者择一但可与 thunk 并存。如果希望进一步降低样板代码、规范 action 写法可继续阅读 docs/usage/ReducingBoilerplate.md含 Actions 一节以及 docs/usage/structuring-reducers/StructuringReducers.mdReducer 组合详解。【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考