深入 Redux掌握超越 combineReducers 的自定义 Reducer 组合逻辑【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/reduxcombineReducers是 Redux 官方内置的 reducer 组合工具但它被刻意设计为只解决一个最常见的场景——把纯 JavaScript 对象形态的状态树按 key 拆分为多个 slice reducer 分别更新。本文基于 BeyondcombineReducers一文展开系统讲解当状态不再是纯对象、slice reducer 之间需要共享数据、或需要控制 reducer 调用顺序时如何手写自定义 reducer 逻辑与高阶 reducer并结合 combineReducers 源码 剖析其行为边界。读完本文你将掌握三种跨 slice 共享数据的实战方案、reduceReducers的组合技巧以及用函数组合构建可复用 reducer 的进阶能力。combineReducers 的定位与边界combineReducers是一个非常实用的工具但它的设计目标被刻意限制在一个常见用例上更新一个纯 JavaScript 对象形态的状态树——把更新每个 state 切片的职责委托给对应的 slice reducer。它不处理以下场景状态树由 Immutable.jsMap等特殊数据结构构成某个 slice reducer 需要把状态树的其他部分作为额外参数传入需要对多个 slice reducer 的调用进行排序ordering它也不关心某个 slice reducer 内部具体如何完成工作。因此面对如何用combineReducers处理这些场景的问题官方答案非常直白你不需要用它你很可能应该换别的方案。一旦越过combineReducers的核心用例就应该使用更自定义的 reducer 逻辑——无论是针对一次性场景的专用逻辑还是可以被广泛复用的通用函数。在动手写自定义逻辑之前先理解combineReducers为什么有这些边界会更有帮助。从源码看 combineReducers 的行为约定combineReducers的实现位于 src/combineReducers.ts其核心行为可以在源码中得到印证1. 初始化时对每个 slice reducer 做形状校验。在创建组合 reducer 时assertReducerShape会先用ActionTypes.INIT探测每个 reducer 的初始状态再用一个随机的私有 action 探测未知 action 的处理如果reducer(undefined, { type: ActionTypes.INIT })返回undefined会抛出错误提示必须显式返回初始状态undefined不行可以用null如果探测未知 action 时返回undefined同样抛错要求未知 action 必须返回当前 state。2. 每次 dispatch 时按固定签名调用全部 slice reducer。生成的combination函数遍历finalReducerKeys对每个 key 执行const previousStateForKey state[key] const nextStateForKey reducer(previousStateForKey, action)注意调用签名是固定的reducer(previousState, action)——这正是无法天然把其他 slice 或整个 state 作为第三个参数传进去的根源。此外若某个 slice reducer 返回undefinedcombination会直接抛出带 action type 与 key 的详细错误。3. 有引用相等性优化。combination通过hasChanged标记判断nextStateForKey ! previousStateForKey只有当至少一个 slice 发生变化或 key 数量变化时才返回新对象否则原样返回旧 state。这也意味着 slice reducer 必须遵循不可变更新约定否则引用比较会失效。在 UsingcombineReducers中还有更完整的约定说明对于任何未识别的 action 必须返回传入的state绝不能返回undefined当state为undefined时必须返回该 slice 的初始状态。而 API 参考 则指出即使你在createStore中传入了preloadedStatecombineReducers依然会用undefined探测所有 reducer因此 reducer 必须能在收到undefined时正常工作。理解了这些行为再看共享数据这类combineReducers覆盖不到的场景就有明确方向了。场景一slice reducer 之间共享数据假设sliceReducerA在处理某个 action 时需要用到sliceReducerB所在 slice 的数据或者sliceReducerB需要拿到整个 state 作为参数。combineReducers本身不处理这种情况官方文档给出了三种典型方案。方案一手写组合 reducer按需传递额外参数写一个自定义的父级 reducer在特定 action 类型下把需要的数据作为额外参数传给目标 slice reducerfunction combinedReducer(state, action) { switch (action.type) { case A_TYPICAL_ACTION: { return { a: sliceReducerA(state.a, action), b: sliceReducerB(state.b, action) } } case SOME_SPECIAL_ACTION: { return { // 专门把 state.b 作为额外参数传入 a: sliceReducerA(state.a, action, state.b), b: sliceReducerB(state.b, action) } } case ANOTHER_SPECIAL_ACTION: { return { a: sliceReducerA(state.a, action), // 专门把整个 state 作为额外参数传入 b: sliceReducerB(state.b, action, state) } } default: return state } }这个方案的关键在于普通的(state, action)二元签名保持兼容仅在明确需要时扩展为三元调用。combineReducers做不到这一点是因为它内部始终以二元签名调用所有 slice reducer见 src/combineReducers.ts 中reducer(previousStateForKey, action)的固定调用。设计文档 Design Decisions FAQ 中也提到给每个 reducer 增加第三个参数存在到底该传整个状态树、回调函数还是其他 slice的歧义问题因此combineReducers才没有内置该能力。方案二把共享数据塞进 action共享 slice 更新的另一个更简单的替代思路是把需要的数据直接放进 action 里。借助 thunk 函数可以在 dispatch 之前用getState()和 selector 取出目标数据function someSpecialActionCreator() { return (dispatch, getState) { const state getState() const dataFromB selectImportantDataFromB(state) dispatch({ type: SOME_SPECIAL_ACTION, payload: { dataFromB } }) } }因为 B slice 的数据已经在 action 里父级 reducer 完全不需要做任何特殊处理sliceReducerA就能通过action.payload.dataFromB拿到它。这个方案把跨 slice 读取提前到了 action 创建阶段reducer 层保持纯粹。方案三combineReducers 处理常规、额外 reducer 处理特例前两种方案都会让父级 reducer 承担较多手工拼接逻辑。第三种方案是分工协作让combineReducers生成的 reducer 处理每个 slice 各自独立更新的简单场景再单独写一个跨 slice 的 reducer 处理需要共享数据的特殊 action最后用一个包装函数依次调用两者const combinedReducer combineReducers({ a: sliceReducerA, b: sliceReducerB }) function crossSliceReducer(state, action) { switch (action.type) { case SOME_SPECIAL_ACTION: { return { // 专门把 state.b 作为额外参数传入 a: handleSpecialCaseForA(state.a, action, state.b), b: sliceReducerB(state.b, action) } } default: return state } } function rootReducer(state, action) { const intermediateState combinedReducer(state, action) const finalState crossSliceReducer(intermediateState, action) return finalState }这里rootReducer扮演了流水线角色先让combinedReducer完成常规更新再把中间状态交给crossSliceReducer处理特例。整个过程仍然是纯函数、顺序确定的。用 reduceReducers 简化流水线组合上述先跑 A 再跑 B的包装模式非常通用社区已有工具化方案——reduceReducers工具。它接受多个 reducer用reduce()依次执行把上一个 reducer 产出的中间状态传给下一个// 与上面手写的 rootReducer 等价 const rootReducer reduceReducers(combinedReducers, crossSliceReducer)需要特别注意的是使用reduceReducers时要确保列表中的第一个 reducer 负责定义初始状态。因为后续的 reducer 通常会假设整个状态已经存在不会主动提供默认值一旦第一个 reducer 不能初始化状态后续 reducer 就可能拿到undefined并出错。这种任务式组合在仓库文档中也有实际用例在 Updating Normalized Data 的 Task-Based Updates 一节中作者把ADD_COMMENT这种跨多个表的更新抽成独立的featureReducers再用reduceReducers(combinedReducer, featureReducers)与常规的combineReducers结果串接实现了普通 action 走 slice 分工、特殊 action 走任务式更新的混合架构。进阶用函数组合构建可复用 reducer理解Redux reducer 只是函数这一点至关重要。combineReducers只是工具箱里的一件工具函数可以包含 switch 之外的条件逻辑、可以被组合包裹、也可以调用其他函数。用 compose 组合可撤销 过滤 切片reducer假设你希望某个 slice reducer 能够重置自身状态并且整体上只响应特定几个 action可以借助函数组合compose来完成Redux 内置compose的实现见 src/compose.ts它从右向左组合函数compose(f, g, h)等价于(...args) f(g(h(...args)))const undoableFilteredSliceA compose( undoReducer, filterReducer(ACTION_1, ACTION_2), sliceReducerA ) const rootReducer combineReducers({ a: undoableFilteredSliceA, b: normalSliceReducerB })这里filterReducer(ACTION_1, ACTION_2)是一个高阶 reducerhigh-order reducer它只把指定类型的 action 放行给内部 reducerundoReducer则为结果增加撤销能力。注意combineReducers完全不知道也不关心管理a的这个函数有多特殊——我们不需要修改combineReducers去理解如何撤销只是把需要的零件组装成一个新的复合函数而已。高阶 reducerreducer 的工厂与包装器在 Splitting Reducer Logic 中给出了明确的术语定义root reducer真正传给createStore的 reducer是唯一必须保持(state, action) - newState签名的部分slice reducer负责更新状态树某个切片的 reducer通常传给combineReducershigher-order reducer接收 reducer 作为参数、或返回新 reducer 的函数如combineReducers、redux-undo可视为reducer 工厂。combineReducers本身就是一个高阶 reducer——它接收一个装满 slice reducer 的对象返回一个新的 reducer。这一模式可以被推广到更多场景例如 Reusing Reducer Logic 中提到的通用过滤高阶 reducerfunction createFilteredReducer(reducerFunction, reducerPredicate) { return (state, action) { const isInitializationCall state undefined; const shouldRunWrappedReducer reducerPredicate(action) || isInitializationCall; return shouldRunWrappedReducer ? reducerFunction(state, action) : state; } } const rootReducer combineReducers({ // 检查带后缀的字符串 counterA : createFilteredReducer(counter, action action.type.endsWith(_A)), // 检查 action 里的附加数据 counterB : createFilteredReducer(counter, action action.name B), // 只响应所有 INCREMENT绝不响应 DECREMENT counterC : createFilteredReducer(counter, action action.type INCREMENT) };注意isInitializationCall的判空处理在combineReducers初始化探测阶段state undefined必须放行否则 slice 的初始状态永远无法建立——这正是前面源码分析中assertReducerShape行为的直接呼应。这类模式可以支撑同一个组件在 UI 中出现多个实例分页、排序等通用能力复用等需求。类似的命名包装器createNamedWrapperReducer、集合/条目 reducer 模式Collection / Item Reducer Pattern在 Reusing Reducer Logic 中有完整示例可作为进阶阅读。生态参考与自研建议combineReducers是 Redux 内置的唯一 reducer 工具函数但社区已经发布了大量可复用的第三方 reducer 工具涵盖批量 action、撤销历史、跨 slice 组合、状态过滤等方向。仓库中的 Ecosystem 文档 集中列举了这类第三方工具如reduceReducers等以及 Redux Addons Catalog 的索引入口建议在动手前先检索是否已有现成方案。如果所有现成工具都无法精确满足你的用例那就自己写一个恰好做你要做的事的函数——毕竟 reducer 只是函数组合它们没有任何魔法。总结combineReducers解决的是纯对象状态树 按 slice 委托更新这一最常见场景其固定签名、初始化探测与引用相等性优化等行为都可以在 src/combineReducers.ts 中得到印证。当需求越过这条边界时正确姿势是转向自定义 reducer 逻辑跨 slice 共享数据手写组合 reducer 传递额外参数、把数据预先放入 actionthunk getState、或组合combineReducers与 cross-slice reducerreducer 编排用reduceReducers依次串联多个 reducer并确保第一个 reducer 负责初始化状态能力复用借助compose与高阶 reducer过滤、命名、撤销包装把通用能力拼装进某个 slicecombineReducers对此完全透明。把握住reducer 只是函数、组合只是函数调用这一原则无论状态形态如何变化都能写出清晰、可复用、可测试的 reducer 架构。【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
