首页
/ Redux 核心概念与数据流:从单向数据流到 Action、Reducer 与 Store 的实现原理

Redux 核心概念与数据流:从单向数据流到 Action、Reducer 与 Store 的实现原理

2026-09-04 13:40:24作者:裴麒琰

本文基于 Redux 官方教程 Fundamentals 第二篇 Redux Concepts and Data Flow,系统讲解 Redux 的术语体系(Action、Reducer、Store、dispatch、Selector)、三大设计原则(单一数据源、状态只读、纯函数更新)以及完整的应用数据流。并结合本仓库的核心源码(createStoreisPlainObject 等)与 counter-vanilla 示例,印证这些概念在实现层面的具体行为与约束。读完你不仅掌握 Redux 的词汇与心智模型,还能从源码级理解"为什么 action 必须是带字符串 type 的普通对象"、"为什么不能在 reducer 中 dispatch"这类规则的存在原因。

单向数据流示意图:状态描述应用在某时刻的形态,UI 基于状态渲染,事件触发状态更新后 UI 重新渲染

Redux 应用数据流动画:UI dispatch action 给 store,store 调用 reducer 计算新状态后通知订阅者

引言

Part 1: Redux 概述 中,我们介绍了 Redux 是什么、为什么使用它,以及配套使用的其他 Redux 库,并看到一个可运行的 Redux 应用长什么样、由哪些部分组成。本篇则深入其中:把 Part 1 里一笔带过的术语与概念逐一展开,并进一步说明数据是如何在一个 Redux 应用中流动的。

背景概念:先理解 Redux 解决什么问题

状态管理与单向数据流

先看一个小的 React 计数器组件。它用组件内部状态跟踪一个数字,并在按钮被点击时递增:

function Counter() {
  // State: a counter value
  const [counter, setCounter] = useState(0)

  // Action: code that causes an update to the state when something happens
  const increment = () => {
    setCounter(prevCounter => prevCounter + 1)
  }

  // View: the UI definition
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}

这是一个自包含的应用,由三部分组成:

  • state(状态):驱动应用的"唯一事实来源";
  • view(视图):基于当前状态的声明式 UI 描述;
  • actions(动作):由用户输入等触发的、引起状态更新的事件。

这是"单向数据流"的一个小例子:

  • 状态描述应用在某一时点的形态;
  • UI 根据该状态渲染;
  • 当某件事发生(如用户点击按钮),状态基于发生的事被更新;
  • UI 基于新状态重新渲染。

但当多个组件需要共享和使用同一份状态、且这些组件分散在应用的不同位置时,这种简单模式就会失效。有时可以通过"状态提升"(lifting state up)把状态上移到父组件来解决,但并不总是可行。

一种解决方式:把共享状态从组件中抽离出来,放到组件树之外的集中式位置。这样,整个组件树就变成了一个大"视图",任何组件无论位于树的哪个位置,都可以访问状态或触发动作。通过定义并分离状态管理中的各个概念、并强制视图与状态之间相互独立的规则,代码获得了更多的结构化与可维护性。

这就是 Redux 的基本思想:在全应用中用一个集中式的 store 保存全局状态,并遵循特定的模式来更新该状态,从而使代码可预测。

Immutability:不可变更新

"Mutable"意为"可变的"。如果某样东西是 "immutable"(不可变的),它就永远无法被改变。

JavaScript 的对象和数组默认都是可变的。创建对象后,可以修改其字段内容;创建数组后,同样可以修改内容:

const obj = { a: 1, b: 2 }
// 对象引用没变,但内部内容变了
obj.b = 3

const arr = ['a', 'b']
// 数组同理,可以修改内部内容
arr.push('c')
arr[1] = 'd'

这叫做对对象或数组的"变异"(mutating)。在内存中它仍是同一个对象/数组引用,但内部内容已经改变。

要以不可变方式更新值,你的代码必须对已有对象/数组做"拷贝",然后修改拷贝出来的副本。

可以手动用 JavaScript 的对象/数组展开运算符(spread)实现,也可以用那些返回新数组副本而非变异原数组的数组方法:

const obj = {
  a: {
    // 要安全地更新 obj.a.c,必须逐级拷贝
    c: 3
  },
  b: 2
}

const obj2 = {
  // 拷贝 obj
  ...obj,
  // 覆盖 a
  a: {
    // 拷贝 obj.a
    ...obj.a,
    // 覆盖 c
    c: 42
  }
}

const arr = ['a', 'b']
// 创建 arr 的新副本,把 "c" 追加到末尾
const arr2 = arr.concat('c')

// 或者,先拷贝原数组:
const arr3 = arr.slice()
// 再修改副本:
arr3.push('c')

Redux 要求所有状态更新都以不可变方式完成。 这一点之所以关键,稍后在数据流与 Store 通知机制中会体现出来。

Redux 核心术语

Action:描述"发生了什么"的普通对象

Action 是一个带有 type 字段的普通 JavaScript 对象。可以把 action 理解为一个事件,描述应用中刚发生的某件事。

type 字段应该是一个给该动作起描述性名称的字符串,例如 "todos/todoAdded"。通常按 "domain/eventName" 的形式书写:前半部分是该 action 所属的功能或类别,后半部分具体发生了什么。

Action 对象还可以携带其他字段,提供关于所发生事件的更多信息。按惯例,这些信息放在一个叫 payload 的字段里。典型的 action 对象如下:

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

这一约定在类型定义中同样被固化下来。在 src/types/actions.ts 中,Action 类型的核心就是 type 字段:

export type Action<T extends string = string> = {
  type: T
}

注释中明确写道:Actions 是获取数据进入 store 的唯一途径,type 必须是字符串,因为字符串是可序列化的。此外仓库还提供了 isAction 工具函数,用于判断一个值是否满足"普通对象且 type 为字符串"这一标准:

export default function isAction(action: unknown): action is Action<string> {
  return (
    isPlainObject(action) &&
    'type' in action &&
    typeof (action as Record<'type', unknown>).type === 'string'
  )
}

Reducer:处理事件的"事件监听器"

Reducer 是一个函数,接收当前的 state 和一个 action 对象,判断是否需要更新状态,并返回新状态:(state, action) => newState。可以把 reducer 看作事件监听器:它根据接收到的 action(事件)的 type 来处理事件。

为什么叫 "Reducer"?因为它们与传给 Array.reduce() 方法的回调函数很相似——详见下文"深入"部分。

Reducer 必须始终遵守几条规则:

  • 只能根据 stateaction 两个参数计算新的状态值;
  • 不允许修改现有的 state,必须通过拷贝现有 state、修改拷贝值的方式做"不可变更新";
  • 不允许做任何异步逻辑、计算随机值,或引起其他"副作用"。

Reducer 内部的逻辑通常遵循同一套步骤:

  • 先判断该 reducer 是否关心这个 action
    • 如果是,拷贝 state,在副本上写入新值并返回
  • 否则,原样返回现有 state

一个展示这些步骤的小型 reducer 示例:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === 'counter/incremented') {
    // If so, make a copy of `state`
    return {
      ...state,
      // and update the copy with the new value
      value: state.value + 1
    }
  }
  // otherwise return the existing state unchanged
  return state
}

Reducer 内部可以使用任意逻辑决定新状态:if/elseswitch、循环等等。

这个约束在类型层面同样体现:src/types/reducers.tsReducer 类型被定义为接收"积累值(状态)与当前值(action)"、返回新积累值的纯函数,其注释强调 Reducer 是 Redux 中最重要的概念,且"不要在 reducer 中放 API 调用"。

深入:为什么它们叫 "Reducer"?

Array.reduce() 方法让你拿到一组值,逐个处理,最终返回一个结果——可以理解为"把数组缩减成一个值"。

Array.reduce() 接收一个回调函数作为参数,数组中每个元素触发一次调用,回调接收两个参数:

  • previousResult:上次回调返回的值;
  • currentItem:当前正在处理的数组元素。

首次执行回调时没有 previousResult,所以还需要传入一个初始值作为第一次的 previousResult

如果要把一组数字相加求和,可以写出这样的 reduce 回调:

const numbers = [2, 5, 8]

const addNumbers = (previousResult, currentItem) => {
  console.log({ previousResult, currentItem })
  return previousResult + currentItem
}

const initialValue = 0

const total = numbers.reduce(addNumbers, initialValue)
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}

console.log(total)
// 15

注意这个 addNumbers "reduce 回调"函数自身不需要记住任何东西:它接收 previousResultcurrentItem,处理之后返回新结果。

Redux 的 reducer 函数与这个 "reduce 回调" 是完全相同的思想:它接收"上次的结果"(即 state)和"当前项"(即 action 对象),基于这两个参数决定新的状态值并返回。

如果我们构造一个 Redux action 数组,调用 reduce() 并传入 reducer 函数,就能得到同样的最终结果:

const actions = [
  { type: 'counter/incremented' },
  { type: 'counter/incremented' },
  { type: 'counter/incremented' }
]

const initialState = { value: 0 }

const finalResult = actions.reduce(counterReducer, initialState)
console.log(finalResult)
// {value: 3}

可以说:Redux reducer 把(时间上的)一串 action 归约成单一状态。 区别在于:Array.reduce() 一次性完成,而 Redux 中这个过程发生在整个应用的生命周期内。

Store:全局状态的容器

当前 Redux 应用状态存放在一个叫做 store 的对象中。

Store 通过传入一个 reducer 来创建,并有一个 getState 方法返回当前状态值:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

这里有一个值得注意的实现细节:教程示例使用的是 Redux Toolkit 的 configureStore,而本仓库核心包中的 createStore 在源码注释里已被标记为 @deprecated,官方建议迁移到 @reduxjs/toolkitconfigureStore(见 src/createStore.ts 的弃用说明)。两者概念一致:把 reducer 交给 store 创建函数,由 store 持有状态树。

从源码看 store 的初始化过程比"保存一个初始值"更微妙。在 createStore 的结尾,创建 store 时会主动 dispatch 一个内部 INIT action:

// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT } as A)

也就是说,初始状态并不是"预先填进去"的,而是 store 用 INIT action 跑一遍 reducer、把返回值作为初始 state 得到的。这正是教程所说"store 调用一次根 reducer,并把返回值保存为初始 state"的实现来源。

Dispatch:更新状态的唯一途径

Store 上有一个 dispatch 方法。更新状态的唯一方式,是调用 store.dispatch() 并传入一个 action 对象。Store 会用保存的 state 执行 reducer 函数、把返回值作为新 state 存起来,随后你调用 getState() 就能拿到更新后的值:

store.dispatch({ type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}

可以把 dispatch 一个 action 理解为在应用中"触发一个事件":某件事发生了,我们想让 store 知道。Reducer 就像事件监听器,当它们听到自己关心的 action,就更新状态作为响应。

dispatch 的底层实现(src/createStore.ts)揭示了它对 action 的严格校验,解释了为什么"action 必须是带字符串 type 的普通对象":

function dispatch(action: A) {
  if (!isPlainObject(action)) {
    throw new Error(
      `Actions must be plain objects. ...`
    )
  }

  if (typeof action.type === 'undefined') {
    throw new Error(
      'Actions may not have an undefined "type" property. ...'
    )
  }

  if (typeof action.type !== 'string') {
    throw new Error(
      `Action "type" property must be a string. ...`
    )
  }

  if (isDispatching) {
    throw new Error('Reducers may not dispatch actions.')
  }

  try {
    isDispatching = true
    currentState = currentReducer(currentState, action)
  } finally {
    isDispatching = false
  }

  const listeners = (currentListeners = nextListeners)
  listeners.forEach(listener => {
    listener()
  })
  return action
}

几个要点:

  • "普通对象"的判定isPlainObject 完成:它沿原型链走到顶端,确认对象的原型就是 Object.prototype(或 null),从而排除类实例、数组之外的"伪对象"。测试用例 test/createStore.spec.ts 验证了 dispatch 一个 Promise、函数、Datenull 时会分别抛出明确描述实际类型('Promise''function''date' 等)的错误。
  • type 必须存在且为字符串:dispatch {}{ type: undefined } 都会抛出 Actions may not have an undefined "type" property(见 test/createStore.spec.ts);type 是布尔、数字等类型时抛出 Action "type" property must be a string
  • reducer 执行期间禁止再 dispatchReducers may not dispatch actions)、禁止 getState()/subscribe(),源码通过 isDispatching 标志位强制这些约束,测试 test/createStore.spec.ts 覆盖了"在 reducer 中途调用 subscribe 会抛错"的场景。
  • 通知订阅者:reducer 执行完毕后,store 遍历当前订阅者快照逐一调用,这正是教程"数据流"中"store 通知所有已订阅的 UI 部分"这一步的实现。

这些校验也解释了为什么 dispatch 的 JSDoc 里说:基础实现只支持普通对象 action;若想 dispatch Promise、Observable 或 thunk,需要引入中间件包装 store(参见本仓库 src/types/store.tsdispatch 的文档注释)。

Selector:从状态中提取数据的函数

Selector 是一类知道如何从 store 状态值中提取特定信息的函数。 随着应用变大,这能帮助避免各处重复读取同一数据的逻辑:

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

Selector 体现了"视图从 store 中按需取数"的模式:组件不直接依赖整个 state 的形状,而是依赖一小段提取逻辑。

三大核心设计原则

Redux 设计的意图可以总结为三个核心概念:

单一事实来源(Single Source of Truth)

应用的全局状态作为一个对象存放在唯一的 store 中。任何一条数据只应存在于一个位置,而不是在多处复制。

这让调试和检查应用状态随时间变化变得更加容易,也让需要与整个应用交互的逻辑得以集中。

提示:这并不意味着应用的每一份状态都必须放进 Redux store!某份状态应属于 Redux 还是 UI 组件,应根据它在哪里被需要来决定。

状态只读(State is Read-Only)

改变状态的唯一方式是 dispatch 一个 action——描述"发生了什么"的对象。

这样 UI 就不会意外覆盖数据,也更容易追溯某次状态更新为何发生。由于 action 是普通 JS 对象,它们可以被记录日志、序列化、存储,并在之后回放以用于调试或测试。

通过纯函数 Reducer 进行修改

为了指定状态树如何基于 action 更新,你编写 reducer 函数。Reducer 是纯函数:接收上一个状态和一个 action,返回下一个状态。像其他函数一样,你可以把 reducer 拆分为更小的函数来分担工作,或为常见任务编写可复用的 reducer。

"纯函数"在实现上意味着同样的输入必然产生同样的输出、且不产生副作用——这也是热重载、时间旅行等特性得以成立的前提(见 src/types/reducers.ts 的类型注释)。

Redux 应用的数据流

前文提到的"单向数据流"描述了更新应用的这一序列:

  • 状态描述应用在某一时点的形态;
  • UI 基于该状态渲染;
  • 当某件事发生(如用户点击按钮),状态基于发生的事被更新;
  • UI 基于新状态重新渲染。

具体到 Redux,可以把这些步骤进一步细化:

初始设置:

  • 使用根 reducer 函数创建一个 Redux store;
  • Store 调用一次根 reducer,把返回值保存为初始 state(对应源码中创建 store 时 dispatch 的 INIT action);
  • UI 首次渲染时,UI 组件访问 Redux store 的当前状态,用这些数据决定渲染什么。同时它们订阅 store 的后续更新,以便在状态变化时获知。

更新:

  • 应用中发生某件事,例如用户点击按钮;
  • 应用代码向 Redux store dispatch 一个 action,如 dispatch({type: 'counter/incremented'})
  • Store 再次调用 reducer 函数,参数是上一份 state 和当前 action,并把返回值保存为新的 state
  • Store 通知所有已订阅的 UI 部分:store 已更新;
  • 每个需要从 store 获取数据的 UI 组件检查自己关心的那部分状态是否变化(这正是不可变更新的意义:只要某分支没变,引用就保持不变,比较引用即可判断);
  • 每个发现自己数据发生变化的组件强制用新数据重新渲染,从而更新屏幕上显示的内容。

上面配图(Redux 数据流示意图)直观展示了这一流程。

源码级的数据流印证

仓库中最贴近这段描述的运行示例是 examples/counter-vanilla/index.html,它不用任何框架,完整呈现了"状态、渲染、订阅、dispatch"四要素:

function counter(state, action) {
  if (typeof state === 'undefined') {
    return 0
  }

  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

var store = Redux.createStore(counter)
var valueEl = document.getElementById('value')

function render() {
  valueEl.innerHTML = store.getState().toString()
}

render()
store.subscribe(render)

document
  .getElementById('increment')
  .addEventListener('click', function () {
    store.dispatch({ type: 'INCREMENT' })
  })

这段代码与教程描述一一对应:Redux.createStore(counter) 创建 store 并触发 INIT 得到初始值 0render() 完成首次渲染;store.subscribe(render) 让 UI 订阅后续更新;按钮点击触发 store.dispatch({ type: 'INCREMENT' }),store 内部调用 currentReducer(currentState, action) 得到新状态,再遍历订阅者调用 render(),完成一次完整的单向数据流循环。

另外,从 createStore 源码 的结构还可以看到一个健壮性细节:store 在每次 dispatch 前会对订阅者列表做快照(ensureCanMutateNextListeners 在必要时浅拷贝 nextListeners),避免在 dispatch 过程中有人 subscribe/unsubscribe 导致正在遍历的监听器列表被意外修改——这保证了"在本次 dispatch 中,所有在 dispatch 开始前注册的订阅者都会被调用"这一行为契约(其 JSDoc 与 src/types/store.ts 中的说明一致)。

本篇小结

  • Redux 的意图可以总结为三条原则
    • 全局应用状态保存在单一的 store 中;
    • store 状态对其余部分只读;
    • 使用 reducer 函数响应 action 来更新状态。
  • Redux 采用"单向数据流"的应用结构
    • 状态描述应用在某时点的形态,UI 基于该状态渲染;
    • 当应用中发生某事时:
      • UI dispatch 一个 action;
      • Store 运行 reducers,状态基于发生的事被更新;
      • Store 通知 UI 状态已变化;
    • UI 基于新状态重新渲染。

现在,你已经熟悉了描述一个 Redux 应用各组成部分的关键概念与术语。接下来,在 Part 3: State, Actions, and Reducers 中,我们将开始动手构建一个真正的 Redux 应用,看这些部件如何协同工作。

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

项目优选

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