什么是 Redux?一篇通俗易懂的介绍 – wiki基地


揭秘 Redux:前端应用状态管理的利器

前言:前端开发的“成长烦恼”——状态管理

想象一下,你正在开发一个复杂的网页应用。它可能是一个在线商城,用户可以在不同的页面查看商品、添加商品到购物车、修改购物车数量、结算;它可能是一个社交媒体平台,用户可以浏览动态、点赞、评论、关注好友;它可能是一个数据看板,需要从多个来源获取数据并动态展示和交互。

最初,当你的应用还比较简单时,每个组件只需要管理自己的内部状态(比如一个按钮是“开启”还是“关闭”,一个输入框的值是什么)。这种状态管理方式(通常称为“组件内部状态管理”)非常直观和方便。

然而,随着应用功能的不断增加和复杂化,你开始遇到一些挑战:

  1. 状态共享的难题: 很多时候,不同的组件需要访问或修改同一份数据。比如购物车里的商品数量,需要在顶部的导航栏显示,在商品详情页显示,在购物车页面显示。如果每个组件都独立维护这份数据,如何保证它们总是同步的?一个地方修改了,其他地方怎么知道并更新?
  2. 组件间的通信复杂化: 当一个组件的状态改变需要通知另一个不直接相关的组件时,传统的父子组件传值(通过 props 逐层向下传递)或子组件通过回调函数通知父组件的方式变得异常繁琐。如果组件层级很深,你可能需要把数据和修改数据的函数一层层地向下“钻”(这就是臭名昭著的“Prop Drilling”),代码变得难以维护和理解。
  3. 状态更新的不可预测性: 在大型应用中,状态的改变可能来自用户交互、网络请求、定时器等多种来源。如果没有一个统一、规范的方式来管理这些改变,应用的运行状态会变得难以预测,bug层出不穷,而且难以追踪。
  4. 调试的困境: 当应用出现问题时(比如数据显示错误),很难确定是哪个组件、哪个操作导致了状态的变化。因为状态可能分散在应用的各个角落,没有一个地方可以清晰地看到所有状态及其变化历史。

这些“成长烦恼”就像一团乱麻,让你的应用变得越来越难以开发、维护和扩展。这个时候,你就需要一种更强大、更系统的方法来管理应用的状态。

这就是 状态管理模式 诞生的原因,而 Redux 则是其中最流行、最经典、也是最具有代表性的一个解决方案。

Redux 是什么?一句话概括:可预测的状态容器

用官方的话来说,Redux 是 JavaScript 应用的一个可预测的状态容器 (A Predictable State Container for JavaScript Apps)

让我们拆解这句话来理解:

  • 状态 (State): 简单来说,就是你的应用在某一刻的数据快照。比如一个用户是否登录、购物车里有哪些商品、当前显示的是哪个页面、一个请求是否正在进行中等等,这些都属于应用的状态。
  • 容器 (Container): Redux 提供了一个集中的地方来存放应用的所有状态。就像一个巨大的仓库,所有需要共享和访问的数据都统一放在这里,而不是分散在各个组件内部。
  • 可预测 (Predictable): 这是 Redux 最核心的价值主张之一。它通过强制遵循特定的规则(后面会详细介绍这三个原则),确保状态的改变总是按照一种可预见的方式发生。给定相同的初始状态和相同的操作序列,你总能得到相同的最终状态。这种可预测性极大地简化了调试、测试和理解应用的行为。

所以,Redux 的核心思想就是:将整个应用的状态存储在一个独立、集中的地方(这个地方在 Redux 里叫做 Store),并且规定状态的改变必须通过一种统一、规范的方式来进行。

它并没有发明什么全新的概念,它更像是一种设计模式的实践,结合了一些函数式编程的思想,提供了一套管理状态的“规矩”。

Redux 的三大核心原则:奠定可预测的基础

为了实现“可预测”这一目标,Redux 强制你的应用遵循三个核心原则。理解了这三个原则,你就理解了 Redux 的精髓:

原则一:单一的真相来源 (Single Source of Truth)

  • 含义: 你的整个应用的状态都被存储在一个只有一个Store 中。
  • 为什么重要:
    • 集中管理: 所有状态都在一个地方,避免了数据分散和不一致的问题。无论哪个组件需要访问状态,都去 Store 里获取。
    • 易于调试: 想要了解应用的当前状态?只需查看 Store 里的数据即可。配合 Redux DevTools,你可以轻松地检查整个应用的状态树。
    • 易于理解: 整个应用的状态结构一目了然,方便开发者理解应用的数据流和状态依赖关系。
  • 类比: 想象一个大型公司的中央档案室或数据库。所有重要的、共享的信息都存放在这里,而不是分散在每个员工的办公桌上。任何人需要查阅信息,都必须去中央档案室。

原则二:状态是只读的 (State is Read-Only)

  • 含义: Store 中的状态是只读的。你不能直接去修改 Store 里的状态对象或其属性。改变状态的唯一方法是触发 (Dispatch) 一个 Action
  • 什么是 Action? Action 是一个简单的 JavaScript 对象,它描述了发生了什么事件。Action 对象中必须包含一个 type 字段,用来表明 Action 的类型(例如:{ type: 'ADD_TODO' }{ type: 'USER_LOGGED_IN', payload: { userId: 123 } })。Action 也可以携带一些额外的数据(通常放在 payload 字段里),用来描述这次事件的更多信息。
  • 为什么重要:
    • 强制通过事件改变状态: 确保所有的状态更新都通过明确的 Action 来描述。这使得状态的变化过程变得透明和可追踪。
    • 防止意外修改: 阻止了应用中的任意部分随意修改状态,避免了难以追踪的 bugs。
    • 记录历史: 由于所有变化都由 Action 触发,我们可以轻松地记录下所有的 Action 序列,从而重现状态的变化过程,实现时间旅行调试等高级功能。
  • 类比: 就像一个公司的流程审批系统。你不能直接修改公司的财务报表(状态),你必须提交一份“报销申请”(Action),这份申请描述了你做了什么事(比如出差)以及需要报销的金额(额外数据)。财务部门会根据这份申请来更新财务报表。

原则三:改变状态是通过纯函数 (Changes are Made with Pure Functions)

  • 含义: Action 只是描述了发生了什么,但并没有说明状态如何改变。负责根据 Action 来计算新的状态的,是被称为 Reducers 的函数。Reducers 必须是纯函数
  • 什么是纯函数? 纯函数满足两个条件:
    1. 给定相同的输入,永远返回相同的输出。
    2. 不会产生副作用(Side Effects)。副作用包括修改函数外部的变量、进行网络请求、修改DOM、调用Math.random()、获取当前时间等。
  • Reducers 的作用: Reducer 函数接收当前的 state 和被 dispatchaction 作为参数,然后返回一个新的 state 对象。它绝不能直接修改传入的 state 参数。
    javascript
    // Reducer 函数的签名通常是这样
    (previousState, action) => newState
  • 为什么重要:
    • 可预测性: 纯函数的特性保证了 Reducer 的行为是可预测的。无论何时何地调用它,只要输入相同,输出就相同。
    • 易于测试: Reducer 函数很容易进行单元测试,因为它们只是简单的输入-输出函数,不需要 Mock 复杂的依赖。
    • 支持时间旅行调试: 由于 Reducer 是纯函数,并且不修改原状态,我们可以轻松地保存状态的历史版本,并使用 Redux DevTools 在这些历史状态之间“穿越”。
    • 支持服务端渲染: 纯函数的特性使得 Reducers 很容易在服务器端执行,用于预渲染应用状态。
  • 类比: 回到公司流程审批的例子。报销申请(Action)提交后,负责审批和记账的会计(Reducer)会接收申请和当前的账本(当前状态)。会计会根据申请的内容,在账本上进行计算和登记,然后生成一份新的账本(新状态)。会计的工作是严格按照公司的会计准则来执行的,不会受外部因素干扰(纯函数),也不会随意涂改旧账本(不修改原状态)。

总结一下这三个原则,Redux 的工作流程可以概括为:

所有状态集中在一个 Store 里 (原则一) -> 状态不能直接改,只能通过触发一个描述变化的 Action (原则二) -> Store 收到 Action 后,会交给 Reducer (一个纯函数) 来处理 (原则三) -> Reducer 根据旧状态和 Action 计算出新状态 -> Store 更新为新状态。

这个循环构成了 Redux 的核心数据流。

Redux 的核心组成部分 (Core Concepts)

基于以上三个原则,Redux 主要由以下几个核心部分组成:

  1. Store (存储库):

    • 是 Redux 应用的心脏。
    • 前面已经提到,它负责存储应用的整个状态树。
    • 一个 Redux 应用只能有一个 Store。
    • Store 提供了几个重要的方法:
      • getState(): 获取当前的整个状态树。
      • dispatch(action): 触发一个 Action,这是改变状态的唯一方式。
      • subscribe(listener): 注册一个监听器函数,当状态发生变化时会被调用。UI 层通常会通过绑定库(如 react-redux)来订阅状态变化并更新视图,而不是直接使用这个方法。
      • replaceReducer(nextReducer): 替换当前的 Reducer,用于热加载等高级功能。
  2. Actions (动作):

    • 是描述发生了什么事件的普通 JavaScript 对象。
    • 必须包含一个 type 属性,值通常是一个字符串常量,用来唯一标识 Action 的类型。
    • 可以包含任何额外的数据来描述事件的细节,通常放在 payload 字段里。
    • 例如:
      javascript
      { type: 'INCREMENT' } // 增加计数器
      { type: 'ADD_ITEM_TO_CART', payload: { itemId: 123, quantity: 1 } } // 添加商品到购物车
      { type: 'FETCH_USERS_SUCCESS', payload: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] } // 获取用户列表成功
    • Action 本身不包含任何逻辑,它只是一个事实的记录。
  3. Reducers (归纳器/化简器):

    • 是一系列纯函数,负责根据当前的 state 和接收到的 action 来计算并返回新的 state
    • 它们的签名是 (previousState, action) => newState
    • 在处理 Action 时,Reducer 通常使用 switch 语句或查找表来判断 Action 的 type,并根据不同的类型执行相应的逻辑。
    • 非常重要的一点: Reducers 绝不能直接修改(mutate)旧的状态对象。它们必须返回一个全新的状态对象。如果状态是嵌套的,你需要使用不可变更新的方式来创建新的对象或数组副本。
      javascript
      // 这是一个简单的 Reducer 示例
      function counterReducer(state = { value: 0 }, action) {
      switch (action.type) {
      case 'INCREMENT':
      // 返回一个全新的状态对象,而不是修改 state.value++
      return { value: state.value + 1 };
      case 'DECREMENT':
      return { value: state.value - 1 };
      default:
      // 如果 Reducer 不关心这个 Action,必须返回当前的状态
      return state;
      }
      }
    • 通常,大型应用会有多个 Reducer,每个 Reducer 负责管理状态树中的一部分。Redux 提供了一个 combineReducers 工具函数,可以将多个小的 Reducer 合并成一个根 Reducer,传递给 Store。
  4. Dispatching (分发):

    • 是触发 Action 的过程。
    • 当你需要改变状态时,你调用 store.dispatch(action) 方法,将 Action 发送给 Store。
    • Store 接收到 Action 后,会把它和当前的状态一起传递给根 Reducer。
    • Reducer 计算出新状态后,Store 会更新其内部的状态,并通知所有订阅者。

Redux 的数据流(The Flow)

现在,让我们将这些概念串起来,看看 Redux 的数据流是怎样工作的:

  1. 用户交互 / 事件发生: 用户在界面上点击了一个按钮,或者一个异步操作(如网络请求)完成了。
  2. UI 层触发 Action: 你的组件代码(通常是通过绑定库,如 react-reduxuseDispatch hook)构建一个 Action 对象,并调用 store.dispatch(action) 方法,将 Action 发送给 Store。
    javascript
    // 在 React 组件中(使用 react-redux 的 useDispatch hook)
    const dispatch = useDispatch();
    // 用户点击按钮,触发一个 Action
    <button onClick={() => dispatch({ type: 'INCREMENT' })}>增加</button>
  3. Store 接收 Action: Store 接收到被 dispatch 的 Action。
  4. Store 调用 Root Reducer: Store 会找到它内部的根 Reducer 函数,并把当前的整个状态树和接收到的 Action 作为参数传递给它:rootReducer(currentState, action)
  5. Reducer 计算新状态:
    • 如果使用了 combineReducers,根 Reducer 会将 Action 分发给各个子 Reducer。
    • 每个子 Reducer 接收自己的那部分状态和整个 Action,然后计算并返回其对应的部分的新状态。
    • 根 Reducer 将所有子 Reducer 返回的新状态合并成一个全新的完整状态树。
    • 核心: Reducer 必须返回一个全新的状态对象,而不是修改旧对象。
  6. Store 更新状态: Store 将其内部保存的状态替换为 Reducer 计算出的新状态。
  7. Store 通知订阅者: Store 会通知所有注册了监听器(通过 subscribe)的组件或模块,状态已经发生了变化。
  8. UI 层更新: 使用绑定库(如 react-reduxuseSelector hook)的组件会检测到它们所关心的那部分状态发生了变化。这些组件会重新渲染,从 Store 获取最新的状态,并更新界面显示。

这个循环是单向的,数据总是按照 Action -> Dispatch -> Reducer -> Store -> UI 的方向流动。这种单向数据流是 Redux 实现可预测性的关键。它使得理解状态变化的原因和结果变得非常容易。

为什么选择 Redux?它的优势在哪里?

了解了 Redux 的工作原理,我们再回顾一下它带来了哪些好处:

  1. 状态的集中与可预测性 (Centralization & Predictability): 如前所述,这是 Redux 的核心价值。所有状态在一个地方,所有变化都通过规范的 Action 和纯粹的 Reducer 进行,这使得应用的行为非常容易预测和理解。
  2. 强大的调试能力 (DevTools): Redux DevTools 是一个浏览器扩展,它提供了难以置信的调试功能。你可以清晰地看到每一个被 dispatch 的 Action、Action 发生前后的状态变化。最重要的是,它支持 时间旅行调试 (Time-Travel Debugging),你可以“回放”之前的 Action 序列,或者“跳跃”到某个特定的状态,就像在调试器里设置断点和回退一样,极大地提高了调试效率。
  3. 易于维护 (Maintainability): 随着应用的增长,Redux 提供的结构和约定使得代码库更容易组织和维护。新的开发者也能更快地理解应用的数据流和业务逻辑。
  4. 易于测试 (Testability): Reducers 是纯函数,非常容易进行单元测试。只需提供输入状态和 Action,断言输出的新状态是否符合预期即可。Action 创建函数(Action Creators)也易于测试。
  5. 支持高级功能 (Advanced Features): Redux 的设计方便集成各种中间件 (Middleware),用于处理异步操作(如网络请求,经典的如 Redux Thunk, Redux Saga)、日志记录、路由同步等。它也天然支持服务器端渲染 (Server-Side Rendering – SSR),因为状态可以在服务器上预构建并在客户端恢复。
  6. 庞大的生态系统和社区 (Ecosystem & Community): 作为最流行的 JavaScript 状态管理库之一,Redux 拥有活跃的社区和丰富的第三方库和工具,这为开发者提供了强大的支持。

Redux 适用于哪些场景?何时应该使用它?

尽管 Redux 提供了很多优势,但它也引入了一些复杂性(尤其是在没有使用 Redux Toolkit 之前,需要写不少“模板代码” – boilerplate)。因此,并不是所有的应用都需要 Redux。在决定是否使用 Redux 时,可以考虑以下因素:

应该考虑使用 Redux 的情况:

  • 应用状态复杂且需要在多个不相关的组件间共享。 如果很多数据需要在应用的各个角落被访问和修改,并且组件层级较深,使用 Redux 会比 Prop Drilling 或 useContext 更清晰。
  • 应用状态变化频繁且逻辑复杂。 如果状态变化由多种交互和异步操作引起,且状态转换逻辑复杂,Redux 的规范化流程和 Reducer 的纯函数特性可以帮助管理这种复杂性。
  • 需要强大的调试能力。 如果对时间旅行调试等高级调试功能有需求,Redux DevTools 是一个巨大的优势。
  • 需要实现一些高级功能,如撤销/重做、状态持久化、服务器端渲染等。 Redux 的架构使其更容易集成这些功能。
  • 多人协作的大型项目。 Redux 提供了统一的状态管理范式,有助于团队成员之间更好地协作和理解代码。

可能不需要使用 Redux 的情况:

  • 应用非常简单。 如果应用状态不多,且主要局限于单个组件内部,或者只需要在父子组件之间传递少量数据,简单的组件内部状态 (useState) 或 React 的 Context API (useContext) 可能就足够了,并且会更简单。
  • 状态主要集中在组件树的某个局部。 如果共享状态的需求只发生在组件树的某个较小的子树内,React 的 Context API 通常是一个更轻量级的选择。
  • 你刚开始学习前端开发。 Redux 有一定的学习曲线。对于初学者来说,先掌握组件内部状态管理和 Context API,理解了状态共享的痛点后再学习 Redux 会更有效。

总的来说,Redux 适合于中大型、复杂、需要高可维护性和可调试性的应用。对于小型应用或状态管理需求不复杂的场景,引入 Redux 可能会带来不必要的开销和复杂性。

Redux Toolkit:现代 Redux 开发的利器

在 Redux 发展的过程中,社区注意到了一些问题,尤其是经典 Redux 设置的复杂性(需要写很多常量、Action Creator、Reducer 的 Switch 语句等),被称为“模板代码”问题。为了解决这个问题,Redux 官方推荐并推出了 Redux Toolkit (RTK)

Redux Toolkit 是什么?

Redux Toolkit 是 Redux 官方推荐的、包含了一系列工具函数的库,旨在简化 Redux 开发流程,减少模板代码,并内置了一些最佳实践。它不是 Redux 的替代品,而是构建在经典 Redux 之上的一个抽象层和工具集。

Redux Toolkit 带来了什么?

  • 简化的 Store 配置: configureStore 函数极大地简化了 Store 的创建过程,自动集成了 Redux DevTools 支持、Redux Thunk (用于处理异步 Action) 等常用中间件,无需手动配置。
  • 简化 Reducer 编写: createSlice 函数可以一次性定义一个 Slice(Slice 包含了相关的 state、reducers 和 action creators),并且内置了 Immer 库,让你可以用“直观的”方式来编写不可变更新的逻辑,而无需手动展开对象和数组。
    “`javascript
    // 使用 createSlice 简化 Reducer 和 Action Creator 的编写
    import { createSlice } from ‘@reduxjs/toolkit’;

    const counterSlice = createSlice({
    name: ‘counter’, // slice 名称,用于生成 action type
    initialState: { value: 0 }, // 初始状态
    reducers: {
    // reducer 方法,内部使用 Immer,可以直接“修改” state
    increment: state => {
    state.value += 1;
    },
    decrement: state => {
    state.value -= 1;
    },
    incrementByAmount: (state, action) => {
    state.value += action.payload;
    },
    },
    });

    export const { increment, decrement, incrementByAmount } = counterSlice.actions; // 自动生成 action creators
    export default counterSlice.reducer; // 生成对应的 reducer
    ``
    * **内置异步处理:**
    createAsyncThunk` 函数简化了处理异步逻辑(如 API 调用)所需的 Action 类型定义和 Thunk 函数的编写。
    * 推荐的模式和最佳实践: Redux Toolkit 推崇“按功能划分 Slice”的模式,让代码组织更清晰。

现代 Redux = Redux + Redux Toolkit

如果你现在开始学习 Redux,强烈建议你直接从 Redux Toolkit 入手。它保留了 Redux 的核心思想和优势,同时大大降低了学习曲线和开发复杂度。现在绝大多数新的 Redux 项目都使用 Redux Toolkit。

Redux 与 UI 框架的结合 (以 React 为例)

Redux 本身是一个独立的库,不依赖于任何特定的 UI 框架(它也可以用于 Vue、Angular 甚至纯 JavaScript 项目)。然而,它最常与 React 一起使用。

为了方便在 React 中使用 Redux,通常会搭配使用官方提供的绑定库 react-redux

react-redux 提供了几个重要的 API:

  • Provider 组件: 这个组件通常放在你的应用根组件的最外层,它接收 Redux 的 Store 作为 store 属性,并将 Store 提供给应用中的所有子组件。
  • useSelector Hook: 在函数式组件中用来从 Redux Store 中提取(选择)你需要的那部分状态。它接收一个选择器函数作为参数,当 Store 中的状态变化时,如果选择器返回的值发生了变化,组件就会重新渲染。
  • useDispatch Hook: 在函数式组件中用来获取 Redux Store 的 dispatch 方法。你可以通过调用这个 hook 获得的 dispatch 方法来触发 Action。

使用 react-redux 提供的 hooks,你可以方便地在 React 组件中连接 Redux Store,获取状态并触发状态更新,而无需手动订阅 Store 或层层传递 dispatch 函数。

“`javascript
// 示例:在 React 组件中使用 react-redux hooks
import React from ‘react’;
import { useSelector, useDispatch } from ‘react-redux’;
import { increment, decrement } from ‘./counterSlice’; // 从前面用 createSlice 生成的 action creators

function Counter() {
// 使用 useSelector 从 store 中获取状态
const count = useSelector(state => state.counter.value);
// 使用 useDispatch 获取 dispatch 函数
const dispatch = useDispatch();

return (

计数器: {count}

);
}

export default Counter;
“`

这就是 Redux 如何与 React 集成的大致方式,通过 react-redux 库,使得在 React 组件中访问和修改 Redux Store 的状态变得非常便捷。

总结与展望

回顾一下,我们了解了为什么前端应用需要状态管理,以及 Redux 是如何通过其 单一的真相来源、状态只读、通过纯函数改变状态 这三大核心原则来解决状态管理中的挑战,实现状态的 可预测性

我们还深入了解了 Redux 的核心组成部分:Store、Actions、Reducers、Dispatching,以及它们之间构成的 单向数据流

我们探讨了 Redux 的主要优势,包括强大的调试能力、易于维护和测试、以及对高级功能的支持。同时,我们也理性地讨论了何时应该使用 Redux,以及何时更简单的方案可能更适合。

最后,我们强调了 Redux Toolkit 是现代 Redux 开发的首选工具,它极大地简化了 Redux 的使用。我们也简要介绍了 Redux 如何通过 react-redux 等绑定库与 React 等 UI 框架结合使用。

掌握 Redux(尤其是 Redux Toolkit)是一个重要的技能,特别是对于开发中大型前端应用的开发者而言。它提供了一种结构化、可维护、可预测的方式来管理应用状态,能够帮助你构建更健壮、更易于协作的应用。

当然,这篇文章只是对 Redux 的一个详细入门介绍。Redux 还有很多更深入的概念,比如中间件 (Middleware)、Action Creators、异步 Action 处理 (如 Thunk, Saga)、选择器 (Selectors) 的优化等等。但有了对核心概念和原则的理解,你就能更有信心地去探索 Redux 的更深层次。

如果你决定在你的下一个项目中使用 Redux,强烈建议你查阅 Redux 官方文档(尤其是 Redux Toolkit 的部分),它们提供了非常详细和优秀的指南和示例。

希望这篇文章能帮助你拨开 Redux 的迷雾,理解它的本质和价值!祝你在前端开发的道路上越走越顺畅!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部