Redux 核心概念:Store、Action、Reducer、单向数据流、纯函数与不可变数据

说实话,Redux 刚出来那会儿,我也觉得它挺绕的。什么 Store、Action、Reducer,一堆新名词。但用久了你会发现,它其实就三板斧。今天咱们把这几个核心概念掰开揉碎,讲清楚。

1. Store:唯一的数据源

Store 就是整个应用的状态仓库。我习惯把它想象成一个「中央数据库」——所有组件需要共享的数据,都放在这里。

在 Redux 里,Store 有且只有一个。这一点很重要。为什么?因为多个 Store 会导致数据同步困难,你想想看,两个 Store 里的数据打架了,你找谁评理去?

核心特征:

  • 单一数据源(Single Source of Truth)
  • 通过 createStore() 创建
  • 提供 getState()dispatch()subscribe() 三个方法
import { createStore } from 'redux'

// 创建一个 Store
const store = createStore(rootReducer)

// 获取当前状态
console.log(store.getState())

// 订阅状态变化
store.subscribe(() => {
  console.log('状态变了:', store.getState())
})

我在项目中遇到过一个问题:有人把多个不相关的数据放在同一个 Store 里,结果 reducer 越写越臃肿。后来我学乖了,用 combineReducers 拆分模块,每个模块管好自己的那一亩三分地。

2. Action:描述「发生了什么」

Action 就是一个普通的 JavaScript 对象。它必须有一个 type 属性,用来描述你要做什么事。

// 一个典型的 Action
const addTodoAction = {
  type: 'ADD_TODO',
  payload: {
    id: 1,
    text: '学习 Redux',
    completed: false
  }
}

嗯,这里要注意:Action 只描述「发生了什么」,不描述「怎么处理」。它就像一个快递单,只告诉你要送什么货,至于怎么分拣、怎么配送,那是别人的事。

个人习惯:我一般用常量来定义 Action Type,比如 const ADD_TODO = 'ADD_TODO'。这样能避免拼写错误,也方便全局搜索。

3. Reducer:决定「状态怎么变」

Reducer 是一个纯函数。它接收当前状态和一个 Action,然后返回一个新的状态。

const initialState = {
  todos: []
}

function todoReducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      }
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      }
    default:
      return state
  }
}

为什么叫 Reducer?因为它和 JavaScript 的 Array.reduce() 是一个道理——把一堆 Action 逐个「归约」成最终的状态。

我曾经踩过的坑:在 Reducer 里直接修改了 state。比如 state.todos.push(newTodo)。结果 UI 死活不更新,排查了半天才发现是可变数据的问题。记住:永远不要修改原来的 state,要返回一个新的对象。

4. 纯函数与不可变数据

这两个概念是 Redux 的基石。说白了:

  • 纯函数:同样的输入,永远得到同样的输出。没有副作用,不修改外部变量。
  • 不可变数据:数据一旦创建,就不能被修改。要改变就创建一个新的。

你想想看,如果 Reducer 不是纯函数,那同样的 Action 在不同时间执行,可能得到不同的结果。这调试起来得多痛苦?

不可变数据的常见操作:

操作 可变方式(不要用) 不可变方式(推荐)
添加数组元素 arr.push(item) [...arr, item]
删除数组元素 arr.splice(index, 1) arr.filter((_, i) => i !== index)
更新对象属性 obj.key = value {...obj, key: value}
嵌套对象更新 obj.a.b.c = value {...obj, a: {...obj.a, b: {...obj.a.b, c: value}}}

我个人习惯用 Immer 这个库来处理不可变数据。它让你可以用「可变」的写法,生成「不可变」的结果。代码会清爽很多。

5. 单向数据流

这是 Redux 最优雅的设计之一。数据流动的方向是固定的,就像一条单行道:

  1. 用户触发一个事件(比如点击按钮)
  2. 组件 dispatch 一个 Action
  3. Store 把 Action 传给 Reducer
  4. Reducer 根据 Action 计算出新的 State
  5. Store 通知所有订阅者(组件)状态已更新
  6. 组件重新渲染

为什么会这样设计?说白了,就是为了让数据变化「可预测」、「可追踪」。你想想看,如果数据可以到处乱改,那 bug 出现的时候,你根本不知道是谁改的。

调试利器:配合 Redux DevTools,你可以看到每一次 dispatch 的 Action、前后的 State 变化。我曾经靠这个工具,半小时定位了一个困扰团队两天的 bug。

6. 把它们串起来

最后,咱们看一个完整的例子,把 Store、Action、Reducer 串起来:

// 1. 定义 Action Type
const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'

// 2. 创建 Action Creator
const increment = () => ({ type: INCREMENT })
const decrement = () => ({ type: DECREMENT })

// 3. 定义 Reducer
const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 }
    case DECREMENT:
      return { count: state.count - 1 }
    default:
      return state
  }
}

// 4. 创建 Store
const store = createStore(counterReducer)

// 5. 订阅状态变化
store.subscribe(() => console.log('当前计数:', store.getState().count))

// 6. 派发 Action
store.dispatch(increment()) // 当前计数: 1
store.dispatch(increment()) // 当前计数: 2
store.dispatch(decrement()) // 当前计数: 1

你看,整个流程非常清晰。数据从 Store 流出,经过组件渲染,用户操作后通过 Action 流回 Store,再由 Reducer 处理更新。这就是单向数据流的精髓。

总结一下:

  • Store 是唯一的数据仓库
  • Action 描述发生了什么
  • Reducer 根据 Action 更新状态
  • 纯函数和不可变数据保证可预测性
  • 单向数据流让调试变得简单

嗯,这些概念刚开始可能有点抽象。但你在项目里用上两三次,就会觉得「原来就这么回事」。下一章咱们聊聊中间件,那才是 Redux 真正强大的地方。