ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redux 基础 Reducer 结构与 State Shape 设计指南

Redux 基础 Reducer 结构与 State Shape 设计指南 Redux 基础 Reducer 结构与 State Shape 设计指南【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux本文基于当前仓库中 BasicReducerStructure.md 展开系统讲解 Redux 应用中唯一根 reducer的职责与书写范式以及顶层状态树State Shape的领域划分原则。读完本文你将掌握从单函数 reducer 到switch 默认参数的标准写法能够依据领域数据Domain Data、应用状态App State与 UI 状态UI State三类数据合理设计 state 结构并理解仓库源码 createStore.ts 与 combineReducers.ts 中与之对应的底层机制。唯一的根 Reducer 与其职责首先要明确一个核心事实你的整个应用实际上只有一个 reducer 函数——就是传给createStore作为第一个参数的函数。仓库源码 createStore.ts 在入口处即校验该参数必须是函数否则抛出Expected the root reducer to be a function错误随后这个函数会被存储为currentReducer每次dispatch时以currentReducer(currentState, action)的方式被调用createStore.ts。这一个唯一的根 reducer 最终需要完成以下几件事处理首次调用第一次调用时传入的state为undefinedreducer 必须先提供一个默认状态值再处理后续的 action判断要做的工作根据传入的旧状态与派发的 action决定需要执行何种更新产出新状态若确实需要变更则创建包含更新数据的新对象/新数组并返回无变更则原样返回若无需任何变化则直接返回现有 state 本身。在 TypeScript 类型层面reducer 被严格定义为(state, action) newState形式的纯函数。见 reducers.tsexport type Reducer S any, A extends Action UnknownAction, PreloadedState S (state: S | PreloadedState | undefined, action: A) S注意state参数类型中显式包含undefined这正是在类型层面提醒你reducer 必须能够处理首次调用时 state 为 undefined的情形。最简单的 reducer 写法单函数声明编写 reducer 逻辑最直白的方式是把所有逻辑放进一个函数声明中function counter(state, action) { if (typeof state undefined) { state 0 // 如果 state 是 undefined用默认值初始化 } if (action.type INCREMENT) { return state 1 } else if (action.type DECREMENT) { return state - 1 } else { return state // 遇到无法识别的 action 时原样返回 } }这个简单函数已经完整满足了上文列出的全部基本要求当没有状态时返回默认值完成 store 的初始化依据action.type判断应做的更新类型并返回新值当无需任何工作时返回之前的 state。标准写法switch 默认参数上述写法中的重复if/else语句很快会让人疲劳因此实践中几乎都改用switch语句同时可以用默认参数值来替代显式的typeof state undefined检查。改造后的版本如下function counter(state 0, action) { switch (action.type) { case INCREMENT: return state 1 case DECREMENT: return state - 1 default: return state } }这就是一个典型 Redux reducer 的基本结构值得注意三个约定default分支必须原样返回state而不是返回undefined这是被仓库源码强制要求的在 combineReducers.ts 的assertReducerShape中会用随机 action 探测每个 reducer若其返回了undefined会直接抛出错误提示对于未知 action 必须返回当前 state初始化时state 0的默认值恰好对应了首次调用传入 undefined的场景保持纯函数特性不修改入参、不产生副作用这正是 Redux 支持热重载hot reloading与时间旅行调试的基础见 reducers.ts 中对 reducer 必须为纯函数的说明。仓库实例examples/todos 中的 reducer当前仓库 examples/todos/src/reducers/todos.js 就是上述结构的真实落地const todos (state [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { id: action.id, text: action.text, completed: false } ] case TOGGLE_TODO: return state.map(todo todo.id action.id ? { ...todo, completed: !todo.completed } : todo ) default: return state } }可以看到它同时体现了不可变更新用展开运算符创建新数组、用map生成新对象与未知 action 返回原 state两条准则。仓库中的 visibilityFilter.js 则展示了用常量VisibilityFilters.SHOW_ALL作为默认参数的另一种初始化方式。State Shape把状态当作数据来组织Redux 鼓励你从需要管理的数据角度思考应用任一时刻的数据集合就是应用的 state其结构与组织方式被称为 shape。state 的 shape 直接决定了你如何组织 reducer 逻辑。一个 Redux state 的树顶通常是普通 JavaScript 对象当然也可以是单个数字、数组或专门的数据结构但绝大多数库都假定顶层值是普通对象。最常见的组织方式是在顶层对象中按领域/切片domain / slice进一步划分数据子树。例如一个基础 Todo 应用的 state 可能是{ visibilityFilter: SHOW_ALL, todos: [ { text: Consider using Redux, completed: true, }, { text: Keep all state in a single tree, completed: false } ] }上例中todos与visibilityFilter都是顶层 key各自代表某类特定概念的数据切片。这一 shape 与仓库 examples/todos/src/reducers/index.js 中用combineReducers({ todos, visibilityFilter })组合出的状态结构完全对应——combineReducers返回的新 reducer 会输出与传入对象键名一致的 state 对象详见 UsingCombineReducers.md 中的示例。三类数据划分大多数应用需要处理多种类型的数据可大致归为三类领域数据Domain data应用需要展示、使用或修改的数据如从服务器取回的全部 Todo应用状态App state与应用行为相关的数据如Todo #5 当前被选中、正在请求获取 Todo 数据UI 状态UI state表示 UI 当前如何显示的数据如EditTodo 模态框当前已打开。关键原则按数据而非 UI 组件树定义 shape因为 store 是应用的核心你应当依据领域数据和应用状态来定义 state shape而不是依据 UI 组件树。例如state.leftPane.todoList.todos这种嵌套就是一个坏设计——todos是整个应用的核心概念而非 UI 的某个局部因此todos切片应当位于状态树顶层。UI 树与 state shape 之间几乎不会存在一一对应关系。唯一的例外是你显式地在 Redux store 中跟踪各类 UI 数据但即便如此UI 数据的结构与领域数据的结构也大概率不同。一个典型应用的 state shape 大致如下{ domainData1 : {}, domainData2 : {}, appState1 : {}, appState2 : {}, ui : { uiState1 : {}, uiState2 : {}, } }这种顶层按领域切片、UI 状态集中收敛的布局让每个顶层 key 都能对应一个独立的 slice reducer从而为下一步使用combineReducers拆分 reducer 逻辑铺平道路参见 SplittingReducerLogic.md 与 UsingCombineReducers.md。深入源码初始化与每个 reducer 都会被调用理解了根 reducer 与 state shape 之后有两个底层机制值得结合源码加深认识。其一store 创建时自动派发 INIT action。在 createStore.ts 中store 创建完成后会立即dispatch({ type: ActionTypes.INIT })从而让 reducer 返回初始状态、填充初始状态树。这就是第一次调用 reducer 时 state 为 undefined、默认参数生效的触发点。如果还传入了preloadedStatecreateStore的第二个参数它会直接作为首次调用时传入的state此时默认参数不再生效——preloadedState优先于 reducer 内部的默认值详细规则见 InitializingState.md。其二combineReducers会调用它包裹的每一个 slice reducer。从源码 combineReducers.ts 可以看到组合后的 reducer 会遍历所有 slice reducer把各自的切片状态与当前 action 传入收集每个切片的新状态并汇总成新的 state 对象它还会通过引用比较判断是否发生变化只有真正变化时才返回新对象否则返回原 state。因此dispatch 一个 action 时是否所有 reducer 都被调用的答案取决于你的根 reducer 是否由combineReducers构成——若是则每个被包裹的 slice reducer 都有机会响应该 action。另外combineReducers在初始化时会调用assertReducerShape强制校验每个 slice reducer 在收到undefined状态时必须返回非undefined的初始值且对未知 action 必须返回当前状态combineReducers.ts。这正是本文强调默认参数初始化与default 分支原样返回两大写法的底层原因——它们不仅是风格约定更是被源码校验的硬性要求。延伸阅读SplittingReducerLogic.md如何把根 reducer 拆分为多个 slice reducerUsingCombineReducers.mdcombineReducers的完整使用方式与 state key 命名陷阱InitializingState.mdpreloadedState与 reducer 默认参数如何协作ImmutableUpdatePatterns.md不可变更新的各种模式StructuringReducers.md整个 reducer 组织系列的索引与前置概念【免费下载链接】reduxA JS library for predictable global state management项目地址: https://gitcode.com/gh_mirrors/re/redux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表