首页
/ Redux 核心机制详解:State、Actions 与 Reducers 的设计、编写与 combineReducers 原理

Redux 核心机制详解:State、Actions 与 Reducers 的设计、编写与 combineReducers 原理

2026-09-04 18:22:38作者:邬祺芯Juliet

本文基于 Redux 官方教程 Fundamentals 系列的 Part 3,系统讲解 Redux 三大基石——State(状态)、Actions(动作)与 Reducers(归约函数):如何把业务需求转化为纯 JS 数据结构的 state,如何设计描述"发生了什么"的 action 对象,如何编写严格遵循"纯函数 + 不可变更新"规则的 reducer 函数,以及如何把庞大的根 reducer 拆分为按"feature"组织的 slice 文件,再用 combineReducers 组装回单一根 reducer。文章会对照当前仓库 redux 5.0.1 的源码(src/combineReducers.tssrc/createStore.ts 等),解释状态初始化、action 校验与"未变化则短路"等底层机制。读完本文,你将能够独立设计 state 结构、写出合规的 reducer 与 action 清单,并理解 combineReducers 在运行时到底做了什么检查。

Redux 5.x 的核心包由 src/index.ts 统一导出,主要 API 包括 combineReducers(本文重点)、applyMiddlewarebindActionCreatorscompose 以及(已被标记弃用的)createStore;对应的类型定义位于 src/types/reducers.tssrc/types/actions.ts

前置知识与学习项目准备

Part 2 已建立的背景

Redux 的价值在于为全局应用状态提供一个唯一的中央存放点(store)。围绕它有两个核心概念:

  • Dispatching(分发):通过 dispatch 派发 action 对象,这是改变状态的唯一途径;
  • Reducer 函数:接收当前 state 与 action,返回新的 state。

Part 3 的任务就是把这两个概念落地为可运行的代码。

教程配套工程与启动方式

官方教程配套了一个预配置的示例工程(React + 内置样式 + 一个假 REST API),你可以按以下方式使用:

  • 在 CodeSandbox 中打开并 fork 官方嵌入的 redux-fundamentals-example-app 工程;
  • 也可以克隆对应的 reduxjs/redux-fundamentals-example-app 仓库,然后执行 npm install 安装依赖、npm start 启动项目;
  • 如果只想看最终成品,官方提供了 tutorial-steps 分支可对照学习。

完成本教程后,官方推荐使用 Redux 的 Create-React-App 模板reduxjs/cra-template-redux)创建新项目,它已预装 Redux Toolkit 与 React-Redux,内置一个"现代化改造版"的 counter 示例(即 Part 1 中看到的那个示例)。不借助模板从零搭建时,步骤如下:

  1. 安装 @reduxjs/toolkitreact-redux 两个包;
  2. 使用 RTK 的 configureStore API 创建 store,并传入至少一个 reducer 函数;
  3. 在应用入口文件(如 src/index.js)中导入 Redux store;
  4. 用 React-Redux 的 <Provider> 组件包裹根组件:
root.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

注意版本现状:本仓库当前 package.jsonredux 的版本为 5.0.1。在 5.x 中,createStore 已被标记为 @deprecated,官方推荐改用 Redux Toolkit 的 configureStore(详见 src/createStore.ts 中的弃用注释,以及 docs/introduction/why-rtk-is-redux-today.md)。本教程为了讲清底层原理,仍以 createStore 为主,但本文所有 state/action/reducer 设计原则与 combineReducers 机制在 configureStore 下完全一致——RTK 的 configureStore 实际上会自动调用 combineReducers

初始工程结构速览

教程示例工程基于 Vite 标准模板改造,/src 目录下的关键文件:

  • index.js:应用入口,渲染主 <App> 组件;
  • App.js:主应用组件;
  • index.css:全局样式;
  • /apiclient.js 是一个 fetch 的轻量封装,支持 GET/POST;server.js 提供假的 REST API 端点,后续章节会用到;
  • /exampleAddons:存放教程后面会用来演示的 Redux 附加组件。

定义 Todo 示例应用的需求

教程用一个经典的 Todo 应用来串联 state/action/reducer 的全部知识点,因为 Todo 应用能覆盖真实应用中最常见的几类操作:维护一个条目列表、处理用户输入、数据变化时刷新 UI。

初始业务需求如下:

  • UI 由三个主要区域组成:
    • 一个输入框,让用户输入新 Todo 条目的文本;
    • 一个展示所有已有 Todo 条目的列表;
    • 一个底部区域,显示未完成的 Todo 数量,并提供筛选选项。
  • 列表条目应带复选框,可切换"completed"状态;还应能为条目从预定义颜色列表中选择一个颜色分类标签,以及支持删除条目;
  • 计数器应根据未完成任务数量做单复数变化:"0 items"、"1 item"、"3 items";
  • 应有两个按钮:把全部 Todo 标记为已完成、清除(删除)所有已完成的 Todo;
  • 提供两种筛选方式:
    • 按 "All" / "Active" / "Completed" 筛选;
    • 按一个或多个颜色筛选,显示标签颜色匹配的 Todo。

最终效果如官方截图所示:

官方教程的 Todo 示例应用最终效果截图

设计 State 值与状态结构

React 与 Redux 的一条核心原则是:UI 应该基于 state 构建。因此设计应用的一个有效思路是,先枚举出描述应用行为所需的全部 state,并尽可能用"最少的值"来描述 UI——state 越少,需要维护和更新的数据就越少。

从需求中提炼 state

概念上,这个应用有两块主要 state:

  • 当前的 Todo 条目列表本身;
  • 当前的筛选项。

另外还需要记录用户在"Add Todo"输入框里正在输入的内容,但这块相对次要,教程留到后面再处理。

每个 Todo 条目需要存储:

  • 用户输入的文本;
  • 表示是否完成的布尔标志;
  • 一个唯一 ID;
  • 一个颜色分类(如果选择了的话)。

筛选行为可以用枚举值描述:

  • 完成状态:"All"、"Active"、"Completed";
  • 颜色:"Red"、"Yellow"、"Green"、"Blue"、"Orange"、"Purple"。

从这些值还可以看出:Todos 属于 app state(应用处理的核心数据),而筛选值属于 UI state(描述应用当前正在做什么的状态)。区分这两类 state 有助于理解它们各自的用途。

根状态结构示例

在 Redux 中,应用状态永远保存在纯 JavaScript 对象和数组里。这意味着 state 中不能放:类实例、Map / Set / Promise / Date 这类内置 JS 类型、函数,或任何非纯 JS 数据。

Redux 的根 state 值几乎总是一个纯 JS 对象,其他数据嵌套其中。据此,本应用的 state 结构为:

  • 一个 Todo 条目对象数组,每个条目包含:
    • id:唯一数字;
    • text:用户输入的文本;
    • completed:布尔标志;
    • color:可选的颜色分类;
  • 筛选选项:
    • 当前的 "completed" 筛选值;
    • 当前选中的颜色分类数组。

完整的 state 示例:

const todoAppState = {
  todos: [
    { id: 0, text: 'Learn React', completed: true },
    { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
    { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
  ],
  filters: {
    status: 'Active',
    colors: ['red', 'blue']
  }
}

需要特别强调:在 Redux 之外拥有其他 state 值是完全允许的! 本例目前足够小,所有状态都放在了 Redux store 里,但正如后续教程会看到的,有些数据并不需要进 Redux(例如"这个下拉框是否打开"、"表单输入框的当前值")。

设计 Actions

Action 是带 type 字段的纯 JS 对象。可以把一个 action 理解为一个"事件",描述应用中刚刚发生的事情。就像基于需求设计 state 结构一样,也可以列出描述"会发生什么"的 action 清单:

  • 基于用户输入的文本添加一个新的 Todo 条目;
  • 切换某个 Todo 的 completed 状态;
  • 为某个 Todo 选择颜色分类;
  • 删除一个 Todo;
  • 把所有 Todo 标记为已完成;
  • 清除所有已完成的 Todo;
  • 更换 "completed" 筛选值;
  • 新增一个颜色筛选;
  • 移除一个颜色筛选。

描述"发生了什么"所需的额外数据,通常放在 action.payload 字段中——它可以是数字、字符串,或包含多个字段的对象。

Redux store 并不关心 action.type 的实际字符串是什么,但你自己的代码会靠 action.type 判断是否需要更新;调试时你也经常会在 Redux DevTools 扩展里查看这些 type 字符串。所以action type 要选得可读、能清楚描述发生了什么,日后排查问题会轻松得多。

基于上面的清单,本应用使用的 8 个 action 为:

  • {type: 'todos/todoAdded', payload: todoText}
  • {type: 'todos/todoToggled', payload: todoId}
  • {type: 'todos/colorSelected', payload: {todoId, color}}
  • {type: 'todos/todoDeleted', payload: todoId}
  • {type: 'todos/allCompleted'}
  • {type: 'todos/completedCleared'}
  • {type: 'filters/statusFilterChanged', payload: filterValue}
  • {type: 'filters/colorFilterChanged', payload: {color, changeType}}

这里大部分 action 只有一项额外数据,直接放进 action.payload 即可。颜色筛选本可以拆成"新增"和"移除"两个 action,但教程故意用一个带额外字段的 action 来表示,以演示 payload 也可以是对象

和 state 数据一样,action 应只包含描述"发生了什么"所需的最小信息

源码印证:action 的最低契约是 { type: T extends string },见 src/types/actions.ts;store 的 dispatch 在运行时会强制校验"action 必须是纯对象"、"type 不能为 undefined"、"type 必须是字符串",见 src/createStore.ts 中的 dispatch 实现——上面清单里的 8 个 action 全部满足这些约束。

编写 Reducers

Reducer 是接收当前 stateaction 两个参数、返回新 state 的函数,即 (state, action) => newState

对应到类型系统,src/types/reducers.ts 中的定义是:

export type Reducer<
  S = any,
  A extends Action = UnknownAction,
  PreloadedState = S
> = (state: S | PreloadedState | undefined, action: A) => S

注意返回类型是 S 而不是 S | undefined——reducer 永远不允许返回 undefined,这一点在 combineReducers 的运行时校验中会被反复强调(见后文"源码剖析")。

创建根 Reducer

一个 Redux 应用实际上只有一个 reducer 函数:传入 createStore 的"根 reducer"(root reducer)。它负责处理所有被派发的 action,并计算每次的完整新 state。

src 文件夹(与 index.jsApp.js 同层)创建 reducer.js。每个 reducer 都需要初始 state,所以先加入几条假数据,再写出 reducer 逻辑骨架:

const initialState = {
  todos: [
    { id: 0, text: 'Learn React', completed: true },
    { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
    { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
  ],
  filters: {
    status: 'All',
    colors: []
  }
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
  // The reducer normally looks at the action type field to decide what happens
  switch (action.type) {
    // Do something here based on the different types of actions
    default:
      // If this reducer doesn't recognize the action type, or doesn't
      // care about this specific action, return the existing state unchanged
      return state
  }
}

reducer 可能在应用初始化时被以 undefined 作为 state 值调用;此时必须提供一个初始 state,后面的 reducer 代码才有东西可操作。Reducer 通常用默认参数语法提供初始 state:(state = initialState, action)

处理 todos/todoAdded

先检查当前 action 的 type 是否匹配目标字符串,然后返回一个包含全部 state 字段的新对象——包括那些没有变化的字段:

function nextTodoId(todos) {
  const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
  return maxId + 1
}

// Use the initialState as a default value
export default function appReducer(state = initialState, action) {
  // The reducer normally looks at the action type field to decide what happens
  switch (action.type) {
    case 'todos/todoAdded': {
      // We need to return a new state object
      return {
        // that has all the existing state data
        ...state,
        // but has a new array for the `todos` field
        todos: [
          // with all of the old todos
          ...state.todos,
          // and the new todo object
          {
            // Use an auto-incrementing numeric ID for this example
            id: nextTodoId(state.todos),
            text: action.payload,
            completed: false
          }
        ]
      }
    }
    default:
      // If this reducer doesn't recognize the action type, or doesn't
      // care about this specific action, return the existing state unchanged
      return state
  }
}

加一个 Todo 要写这么多代码,为什么?这就引出了 reducer 必须遵守的规则。

Reducer 的三条铁律

Reducer 必须始终遵守若干特殊规则:

  • 只能基于 stateaction 两个参数计算新 state 值;
  • 不允许修改现有的 state,必须做不可变更新(immutable updates)——复制现有 state,再对复制出来的值做修改;
  • 不能执行任何异步逻辑或其他"副作用"。

"副作用"指任何在"从函数返回值之外"可观察到的状态或行为变化,常见的副作用包括:

  • 向控制台打印日志;
  • 保存文件;
  • 设置异步定时器;
  • 发起 HTTP 请求;
  • 修改函数外部的某个状态,或原地修改(mutate)函数的参数;
  • 生成随机数或唯一随机 ID(如 Math.random()Date.now())。

凡是满足这些规则的函数都叫**"纯"函数**,即使它并不是以 reducer 的形式写出来的。

为什么这些规则如此重要?原因有几方面:

  • Redux 的目标之一是可预测性:当函数的输出只由输入参数决定时,更容易理解它的行为,也更容易测试;
  • 反之,如果函数依赖外部变量或行为随机,你就永远不知道运行结果会是什么;
  • 如果函数修改了其他值(包括参数),会出乎意料地改变应用行为——这是常见 bug 来源,比如"我明明更新了 state,为什么 UI 该刷新时却不刷新";
  • Redux DevTools 的部分能力(如时间旅行调试)依赖 reducer 正确遵守这些规则。

其中"不可变更新"这条规则尤其重要,值得单独展开。

Reducer 与不可变更新

Redux 中的两个基本概念是"mutation(变更/原地修改)"与"immutability(不可变性,把值视为不可更改)"。

在 Redux 中,reducer 绝对不允许修改原始/当前的 state 值!

// ❌ Illegal - by default, this will mutate the state!
state.value = 123

为什么在 Redux 中不能 mutate state?

  • 会引发 bug,例如 UI 无法正确更新到最新值;
  • 更难理解 state 为什么、如何被更新;
  • 更难编写测试;
  • 破坏"时间旅行调试"的正确运行;
  • 违背 Redux 的设计初衷与使用模式。

既然不能改原始值,返回更新后的 state 该怎么做?

Reducer 只能对原始值做"复制",然后才可以修改副本。

// ✅ This is safe, because we made a copy
return {
  ...state,
  value: 123
}

Part 2 已经介绍过手动写不可变更新的方式:使用 JS 的数组/对象展开运算符,以及其他返回原值副本的函数(如 mapconcat)。

当数据是嵌套的时这会变难。不可变更新的一条关键规则是:必须对每一个需要更新的嵌套层级都做一次复制。

如果你觉得"手写这种不可变更新既难记又容易错"——没错,官方也承认手写确实困难,而且 在 reducer 中不小心 mutate state 是 Redux 用户犯下的第一大错误

好消息:在实际应用中,你不需要手写这些复杂的嵌套不可变更新。教程 Part 8(Modern Redux with Redux Toolkit,见 part-8)会讲如何用 Redux Toolkit(如 createSlice 内部基于 Immer)简化 reducer 中的不可变更新写法。

继续处理更多 Actions

在掌握上述规则后,继续往根 reducer 中加逻辑。先按 ID 切换某个 Todo 的 completed 字段:

export default function appReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/todoAdded': {
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: nextTodoId(state.todos),
            text: action.payload,
            completed: false
          }
        ]
      }
    }
    case 'todos/todoToggled': {
      return {
        // Again copy the entire state object
        ...state,
        // This time, we need to make a copy of the old todos array
        todos: state.todos.map(todo => {
          // If this isn't the todo item we're looking for, leave it alone
          if (todo.id !== action.payload) {
            return todo
          }

          // We've found the todo that has to change. Return a copy:
          return {
            ...todo,
            // Flip the completed flag
            completed: !todo.completed
          }
        })
      }
    }
    default:
      return state
  }
}

再看一个筛选相关的 case,处理"可见性筛选值变化"(status 筛选)action:

export default function appReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/todoAdded': {
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: nextTodoId(state.todos),
            text: action.payload,
            completed: false
          }
        ]
      }
    }
    case 'todos/todoToggled': {
      return {
        ...state,
        todos: state.todos.map(todo => {
          if (todo.id !== action.payload) {
            return todo
          }
          return {
            ...todo,
            completed: !todo.completed
          }
        })
      }
    }
    case 'filters/statusFilterChanged': {
      return {
        // Copy the whole state
        ...state,
        // Overwrite the filters value
        filters: {
          // copy the other filter fields
          ...state.filters,
          // And replace the status field with the new value
          status: action.payload
        }
      }
    }
    default:
      return state
  }
}

此时只处理了 3 个 action,代码已经有些长了。如果继续把每个 action 都塞进这一个 reducer 函数,整体会越来越难读——这就是拆分 reducer 的动机。

Reducer 通常会被拆分为多个更小的 reducer 函数,以便理解和维护 reducer 逻辑。

拆分 Reducers:slice 文件与 feature 组织

Redux reducer 通常按"它更新哪一块 state"来拆分。本应用的 state 有两个顶层区域:state.todosstate.filters,因此可以把大的根 reducer 拆成两个小 reducer——todosReducerfiltersReducer

官方建议按"feature"(与某个概念或业务区域相关的代码)组织 Redux 应用的文件夹与文件。某个 feature 的 Redux 代码通常写成单个文件,即"slice 文件",其中包含该部分 app state 的全部 reducer 逻辑与所有 action 相关代码。

因此,管理 state 某一节的 reducer 被称为 "slice reducer"。通常,部分 action 对象与某个 slice reducer 紧密相关,其 action type 字符串应以该 feature 名开头(如 'todos'),并描述发生的事件(如 'todoAdded'),拼接为一个字符串('todos/todoAdded')——这正是前文 8 个 action 命名的由来。

在工程中创建 features 文件夹,其下建 todos 文件夹,再创建 todosSlice.js,把 Todo 相关的初始状态剪切粘贴过来:

const initialState = [
  { id: 0, text: 'Learn React', completed: true },
  { id: 1, text: 'Learn Redux', completed: false, color: 'purple' },
  { id: 2, text: 'Build something fun!', completed: false, color: 'blue' }
]

function nextTodoId(todos) {
  const maxId = todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1)
  return maxId + 1
}

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    default:
      return state
  }
}

然后拷贝 Todo 的更新逻辑过来,但这里有一个重要区别:这个文件只负责更新 todos 相关的 state——它不再嵌套了! 这也是拆分 reducer 的另一个好处:由于 todos state 本身就是一个数组,slice 内部无需再复制外层的根 state 对象,reducer 因此更易读。

这种把多个 reducer 组合起来的方式叫 reducer composition(reducer 组合),是构建 Redux 应用的基本模式。

处理完两个 action 后,todosSlice.js 变为:

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/todoAdded': {
      // Can return just the new todos array - no extra object around it
      return [
        ...state,
        {
          id: nextTodoId(state),
          text: action.payload,
          completed: false
        }
      ]
    }
    case 'todos/todoToggled': {
      return state.map(todo => {
        if (todo.id !== action.payload) {
          return todo
        }

        return {
          ...todo,
          completed: !todo.completed
        }
      })
    }
    default:
      return state
  }
}

代码变短了,也更好读了。

接下来对筛选逻辑做同样的事:创建 src/features/filters/filtersSlice.js,把筛选相关代码移过去:

const initialState = {
  status: 'All',
  colors: []
}

export default function filtersReducer(state = initialState, action) {
  switch (action.type) {
    case 'filters/statusFilterChanged': {
      return {
        // Again, one less level of nesting to copy
        ...state,
        status: action.payload
      }
    }
    default:
      return state
  }
}

这里仍然要复制包含 filters state 的对象,但由于嵌套层级少了一层,逻辑更直白。

官方教程为控制篇幅,跳过了其余 action(如 todos/colorSelectedtodos/todoDeletedtodos/allCompletedtodos/completedClearedfilters/colorFilterChanged)的 reducer 更新逻辑展示,建议读者对照前文的需求清单自行练习;卡住时可以查阅教程末尾给出的 CodeSandbox 中的完整实现。

组合 Reducers:手写根 reducer 与 combineReducers

现在有了两个独立的 slice 文件、各自拥有一个 slice reducer。但前文说过,创建 store 时需要一个根 reducer。如何在不把代码写回一个巨型函数的前提下回到"单一根 reducer"?

由于 reducer 就是普通 JS 函数,可以把两个 slice reducer 导入 reducer.js,写一个只负责调用另外两个函数的新根 reducer:

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

export default function rootReducer(state = {}, action) {
  // always return a new object for the root state
  return {
    // the value of `state.todos` is whatever the todos reducer returns
    todos: todosReducer(state.todos, action),
    // For both reducers, we only pass in their slice of the state
    filters: filtersReducer(state.filters, action)
  }
}

注意:每个 reducer 各自管理全局 state 的一块。每个 reducer 的 state 参数都不同,对应它自己所管理的那块 state。

这就让我们能够按 feature、按 state slice 拆分逻辑,保持可维护性。

combineReducers

观察上面手写的根 reducer:它对每个 slice 做的是同一件事——调用 slice reducer、传入该 reducer 所持有的 state 切片、把结果写回根 state 对象。如果再加 slice,这个模式会一直重复。

Redux 核心库自带一个工具函数 combineReducers,替我们完成这步样板代码。可以用它生成的更短的根 reducer 替换手写版本。

到这一步,真正需要安装 Redux 核心库了

npm install redux

安装后导入并使用 combineReducers

import { combineReducers } from 'redux'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const rootReducer = combineReducers({
  // Define a top-level state field named `todos`, handled by `todosReducer`
  todos: todosReducer,
  filters: filtersReducer
})

export default rootReducer

combineReducers 接收一个对象:键名会成为根 state 对象的键名,值是知道如何更新对应 state 切片的 slice reducer 函数

记住:你传给 combineReducers 的键名,决定了 state 对象的键名!

当前仓库自带的示例 examples/todos-with-undo/src/reducers/index.js 就是这一模式的最小化实践:

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

const todoApp = combineReducers({
  todos,
  visibilityFilter
})

export default todoApp

源码剖析:combineReducers 在运行时做了什么

文档说 combineReducers "替我们做了样板工作",但它实际还内置了一组帮助初学者避坑的校验逻辑。下面结合 src/combineReducers.ts 的实现逐条对照。

1. 过滤非函数值并告警

combineReducers 首先遍历传入对象的键,只有 typeof reducers[key] === 'function' 的值才会进入 finalReducers;在开发环境下(process.env.NODE_ENV !== 'production'),若某个键的值是 undefined,会输出 No reducer provided for key "xxx" 的 warning(见 src/combineReducers.ts)。对应的测试见 test/combineReducers.spec.tsignores all props which are not a functionwarns if a reducer prop is undefined 两个用例分别验证了"非函数属性被忽略"和"undefined 触发警告"。

2. 两把"探针"检查 reducer 契约

assertReducerShape(见 src/combineReducers.ts)会对每个 slice reducer 做两次探针调用:

  • reducer(undefined, { type: '@@redux/INIT' }):如果返回 undefined,抛出错误——"slice reducer 在初始化时返回了 undefined……当传入的 state 为 undefined 时,必须显式返回初始 state。初始 state 不能是 undefined"。这解释了教程中反复强调的 (state = initialState, action) 默认参数写法:store 初始化时就会以 undefined 调用每个 reducer;
  • reducer(undefined, { type: 随机探针 type }):如果返回 undefined,抛出错误——"不要用 slice reducer 处理 'redux/*' 命名空间下的 @@redux/INIT 等私有 action;对任何未知 action 你必须返回当前 state(若 state 为 undefined 则返回初始 state)"。

这两条校验对应了 docs/api/combineReducers.md "Notes" 一节列出的规则:未识别的 action 必须原样返回 state;永远不能返回 undefined;收到 undefined state 时必须返回该 reducer 的初始 state。测试 test/combineReducers.spec.ts 中的 throws an error if a reducer returns undefined handling an actionthrows an error on first call if a reducer returns undefined initializing 用例验证了这两条路径。

3. 未变化的 slice 不重建根 state

组合 reducer 的核心循环(见 src/combineReducers.ts)对每个 key 依次执行 reducer(previousStateForKey, action),并用引用比较判断变化:

nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
// ...
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state

也就是说:只要某个 slice 对该 action"不感兴趣"而返回了原引用,且没有任何 slice 的引用发生变化、键数量也没变,组合 reducer 就直接返回旧的根 state 对象,而不是新建一个根对象。 这个"短路"行为有两个好处:一是为上层(如 React-Redux 的浅比较订阅)保留了"引用未变即无需重渲染"的优化空间;二是配合 dispatchcurrentState = currentReducer(currentState, action) 的赋值(见 src/createStore.ts),让"无变化的 action 不产生新引用"成为可能。

另外,若某个 slice 对某个 action 返回了 undefined,这里会抛出带 action type 与 key 名的详细错误(the slice reducer for key "xxx" returned undefined...,见 src/combineReducers.ts),把"忘记 return state"这类错误尽早暴露。

4. 开发环境下的 state 形状检查

在开发环境,组合 reducer 还会调用 getUnexpectedStateShapeWarningMessage(见 src/combineReducers.ts)检查传入 state 的形状:如果 state 不是纯对象(借助 src/utils/isPlainObject.ts 判断),或含有任何不在 reducer 键列表中的"意外键",会输出警告(同一个意外键只警告一次,通过 unexpectedKeyCache 去重;@@redux/REPLACE action 则被豁免)。这对使用 preloadedState、服务端渲染水合等场景尤其有帮助。

5. 与 createStore 的联动:INIT 与 dispatch 校验

把视角拉回到 store。src/createStore.ts 在函数末尾执行 dispatch({ type: ActionTypes.INIT })(见 src/createStore.ts),注释明确说明:"当 store 被创建时,会派发一个 INIT action,让每个 reducer 返回其初始 state,从而填充初始 state 树。" 这就是教程中"reducer 可能以 undefined 被调用"的来源,也是 combineReducers 第一把探针(用 @@redux/INIT 探测)与之呼应的原因。

dispatch 本身则负责把"action 必须是纯对象且带字符串 type"这条契约落地(见 src/createStore.ts):

  • 非纯对象(如 thunk 函数、Promise)会报错,提示可添加 redux-thunk 等中间件;
  • typeundefined 或非字符串会报错;
  • 在 reducer 执行期间再调用 dispatchgetStatesubscribe 都会被禁止(Reducers may not dispatch actions. 等错误),从机制上强化了"reducer 是纯函数"的规则。

类型层面,combineReducers 的重载(见 src/combineReducers.ts)会基于传入的 reducers 映射推断出组合 state 类型 StateFromReducersMapObject<M> 与 action 联合类型 ActionFromReducersMapObject<M>(定义于 src/types/reducers.ts),因此 combineReducers({ todos: todosReducer, filters: filtersReducer }) 的返回类型会自动携带 { todos: Todo[], filters: Filters } 这样的结构,preloadedState 也允许是 Partial 形状——这与"state 结构由键名决定"的运行时语义完全一致。

小结:State、Actions、Reducers 是 Redux 的基石

每个 Redux 应用都有 state 值、用于描述"发生了什么"的 action、以及基于先前 state 与 action 计算新 state 的 reducer 函数。官方教程对本部分要点的归纳如下:

  • Redux 应用使用纯 JS 对象、数组与原始值作为 state 值
    • 根 state 值应是一个纯 JS 对象;
    • state 应只包含让应用工作所需的最小数据;
    • 类、Promise、函数及其他非纯值不应该进入 Redux state;
    • Reducer 中不允许创建 Math.random()Date.now() 这类随机值;
    • Redux store 之外完全可以并存其他 state(如组件局部 state)。
  • Action 是带 type 字段的纯对象,描述发生了什么
    • type 应是可读字符串,通常写作 'feature/eventName'(如 'todos/todoAdded');
    • Action 可携带其他值,通常放在 action.payload
    • Action 应只携带描述"发生了什么"所需的最小数据。
  • Reducer 形如 (state, action) => newState,必须始终遵守:
    • 只基于 stateaction 参数计算新 state;
    • 永远不 mutate 现有 state,始终返回副本;
    • 不做 HTTP 请求、异步逻辑等副作用。
  • Reducer 应当拆分以便阅读
    • 通常按顶层 state 键(即 state 的"切片")拆分;
    • 通常写在 "slice" 文件中,按 "feature" 文件夹组织;
    • 可以用 Redux 的 combineReducers 组合;
    • 传给 combineReducers 的键名决定了顶层 state 对象的键。

至此,我们已经拥有了一套能更新 state 的 reducer 逻辑,但这些 reducer 本身不会做任何事——它们需要放进一个 Redux store,由 store 在事件发生时带着 action 调用它们。下一篇 Part 4(Store,见 part-4)将讲解如何创建 Redux store 并让 reducer 逻辑真正跑起来。

延伸阅读(仓库内路径)

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384