Redux

19 阅读19分钟

启源

服务端渲染MVC

Web 开发的早期阶段,服务端渲染(SSR) 是绝对的主流模式,浏览器的角色仅是 “渲染页面的容器”,用户发起请求后,服务器需要组装出包含完整数据的 HTML 页面,再整体响应给客户端。

为化解服务器端处理 "请求 - 数据 - 视图" 的复杂度,MVC 模式应运而生

MVC

MVC架构图

:上图展示的是 MVC 模式的通用交互关系。在实际实现中,不同场景下 MVC 的交互方式可能有所不同。

MVC 模式的核心在于将应用分为三个职责明确的组件:

  • Model(模型) :负责数据和业务逻辑

    • 处理数据的存储、检索和验证
    • 封装业务规则和数据操作
    • 不关心数据的展示方式
  • View(视图) :负责用户界面的展示

    • 将数据渲染成 HTML 页面
    • 不包含业务逻辑,只负责展示
    • 在服务端 MVC 中,通常被动接收 Controller 传递的数据
  • Controller(控制器) :负责协调 Model 和 View

    • 接收用户请求并解析
    • 调用 Model 处理业务逻辑
    • 协调 Model 和 View 的交互
    • 作为 Model 和 View 之间的桥梁,实现两者的解耦

服务端MVC模式下浏览器与服务端交互流程:

在服务端MVC实现中,View通常是被动的,数据通过Controller中转,以保持关注点分离。

前端架构模式的发展

随着前端技术的发展,各种架构模式被引入到前端开发中:

  • MVC(Model-View-Controller) :从服务端引入,将应用分为模型、视图和控制器
  • MVP(Model-View-Presenter) :MVC 的变体,Presenter 替代 Controller,View 和 Model 完全解耦
  • MVVM(Model-View-ViewModel) :通过数据绑定实现 View 和 ViewModel 的双向绑定,如 Angular、Vue

无论采用哪种架构模式(MVC、MVP、MVVM),前端开发都面临以下两个挑战:

前端状态管理的挑战

1. 用户操作处理的复杂性

前端需要处理用户的各种操作,这些操作场景复杂多样:

  • 用户交互事件(点击、输入、滚动等)
  • 异步操作(API 请求、定时器等)
  • 状态管理(表单状态、UI 状态等)
  • 路由导航
  • 数据同步和缓存

这些复杂性是前端开发固有的,不依赖于特定的架构模式。

2. 数据流的局限性

对于那些组件化的库(比如 Vue、React),它们使用的是单向数据流。若需要共享数据,则必须将数据提升到顶层组件,然后数据再一层一层传递,极其繁琐。

虽然可以使用上下文(Context) 来提供共享数据,但对数据的操作难以监控,容易导致:

  • 调试错误的困难
  • 数据还原的困难
  • 若开发一个大中型项目,共享的数据很多,会导致上下文中的数据变得非常复杂,难以维护

前端需要一个独立的数据解决方案

为了解决前端状态管理中的这些挑战,社区提出了多种数据管理方案:

Flux

Flux 是 Facebook 提出的一种应用架构模式,它采用单向数据流来解决前端状态管理的问题。

Flux 组成:

  • Action(动作) :一个普通的对象,用于描述要执行的操作。Action 是触发数据变化的唯一原因
  • Store(数据仓库) :用于存储共享数据,可以根据不同的 Action 更改仓库中的数据。

Redux

Redux 是在 Flux 基础上发展而来的状态管理库,它引入了 reducer 的概念,通过纯函数(没有副作用、给定相同输入总是返回相同输出的函数)来管理状态变更,使得状态管理更加可预测和可调试。 Redux 组成:

  • Action(动作) :描述发生了什么的对象

    • Dispatch(分发) :发送 Action 到 Reducer 的方法,用于触发状态更新
  • Reducer(归约器) :纯函数,根据 Action 和当前状态计算新状态

  • Store(数据仓库) :存储应用状态的单一数据源

    • State(状态) :描述应用的状态,包含所有数据,让应用能访问当前状态

      • 访问方式

        • 可以通过 store.getState() 直接访问
        • 在 React 中更推荐使用 useSelector Hook 或 connect 来订阅状态变化,实现响应式更新
    • Store 的方法

      • getState():获取当前状态
      • dispatch(action):分发一个 action,触发状态更新
      • subscribe(listener):注册一个监听器(无参函数),当dispatch分发一个 action 之后,会运行注册的监听器。我们可以通过监听器来监听数据的变化(比如可在其中打印日志记录)。该函数会返回一个函数,用于取消监听
      • replaceReducer(nextReducer):替换掉当前的 reducer(高级用法,很少使用)

Redux 数据流:

4444.png


为了更好地理解 Redux,将Redux 与 MVC 架构模式的类比:

  • Store(数据仓库) 相当于 Model(模型) :两者都负责存储和管理应用的数据状态
  • Action + Reducer(动作 + 归约器) 相当于 Controller(控制器) :两者都负责处理业务逻辑和状态变更
  • View(视图) 仍然是 View(视图) :负责用户界面的展示
示例:假设有一个账号密码组件,以登录功能为例

555.png

Redux 基本使用

介绍 Redux 的核心概念和传统 API(如 createStore),有助于理解 Redux 的工作原理。但在实际开发中,推荐使用 Redux Toolkit(详见下方"Redux Toolkit"部分),它提供了更简洁、更现代的 API。

安装

npm install redux

创建 Store

Store 是 Redux 应用的核心,通过 createStore 创建。

重要说明:createStore 的现状

⚠️ 注意

  • createStore 是 Redux 的传统 API,用于理解 Redux 的核心概念
  • Redux 官方更推荐使用 Redux Toolkit 的 configureStore 来创建 store

传统方式(了解即可):

// store/index.js
import { createStore } from 'redux'
import counterReducer from './reducers/counterReducer'

// 基本用法
const store = createStore(counterReducer)

// 如果需要使用 Redux DevTools(浏览器扩展)
const store = createStore(
  counterReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)

export default store

Redux DevTools:

  • Redux DevTools 是一个浏览器扩展,用于调试 Redux 应用
  • 可以查看 action 历史、状态变化、时间旅行调试等
  • configureStore 会自动配置 DevTools,无需手动设置
  • 传统方式需要手动配置(如上所示)

创建 Action

Action 是一个描述发生了什么的平面对象必须包含 type 属性,该属性用于描述操作的类型。

// actions/action-type.js (这种写法解决硬件编码带来的困扰)
export const INCREASE = Symbol("increase");
export const DECREASE = Symbol("decrease");
export const INCREMENT_BY_AMOUNT = Symbol("increase_by_amount");


// actions/counterActions.js
/**
 若使用action创建函数   应为无副作用的纯函数
**/
export const increment = () => ({
  type: 'INCREMENT'
})

export const decrement = () => ({
  type: 'DECREMENT'
})

export const incrementByAmount = (amount) => ({
  type: 'INCREMENT_BY_AMOUNT',
  payload: amount
})

创建 Reducer

Reducer 必须是一个没有副作用的纯函数,接收当前状态和 Action,返回新状态。

  • 内部通常使用 switch 语句来判断 action.type 值(也可以用 if-else,但 switch 更常见)
  • 一个 store 有且仅有一个 root reducer:Redux 规定一个应用中只能有一个 store,而每个 store 只能有一个 root reducer
  • combineReducers:在大型项目中,通常需要将功能模块拆分成多个独立的 reducer。combineReducers 用于合并多个 reducer,生成一个 root reducer。合并后的 root reducer 管理一个状态对象,该对象的每个属性由对应的子 reducer 管理
// reducers/counterReducer.js
const initialState = {
  count: 0
}

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'DECREMENT':
      return { ...state, count: state.count - 1 }
    case 'INCREMENT_BY_AMOUNT':
      return { ...state, count: state.count + action.payload }
    default:
      return state
  }
}

export default counterReducer
reducer执行的时机
  • 创建一个 store 的时候,会调用一次 reducer(可以利用这一点,用 reducer 初始化状态。-->创建store时不传递任何默认状态,而是将默认值写state上)
  • 通过 store.dispatch,分发了一个 action,此时,会调用 reducer

示例:利用 reducer 初始化状态

// store/index.js
import { createStore } from 'redux'
import counterReducer from './reducers/counterReducer'

// 创建 store 时,Redux 内部会调用一次 reducer(undefined, { type: '@@redux/INIT' })
// 此时 state 为 undefined,会使用默认值 initialState
// 因此 store 的初始状态就是 { count: 0, message: '计数器已初始化' }
const store = createStore(counterReducer)

console.log(store.getState())
// 输出: { count: 0, message: '计数器已初始化' }
// 这说明创建 store 时,reducer 已经被调用了一次,完成了状态初始化
// reducers/counterReducer.js
const initialState = {
  count: 0,
  message: '计数器已初始化'
}

// reducer 的第一个参数 state 有默认值 initialState
// 当创建 store 时,Redux 会调用一次 reducer,传入 undefined 作为 state
// 此时会使用默认值 initialState,从而完成状态的初始化
function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'DECREMENT':
      return { ...state, count: state.count - 1 }
    default:
      return state  // 创建 store 时,action.type 是特殊值,会返回 initialState
  }
}

export default counterReducer

关键点:

  • 创建 store 时,Redux 会调用 reducer(undefined, { type: '@@redux/INIT' })
  • 此时 stateundefined,reducer 会使用默认参数 initialState
  • 因此不需要在 createStore 时手动传递初始状态,而是通过 reducer 的默认参数来初始化
多个 Reducer 组合
// reducers/index.js
import { combineReducers } from 'redux'
import counterReducer from './counterReducer'
import todoReducer from './todoReducer'

const rootReducer = combineReducers({
  counter: counterReducer,
  todos: todoReducer
})

export default rootReducer
// store/index.js
import { createStore } from 'redux'
import rootReducer from './reducers'

const store = createStore(rootReducer)

export default store

基本使用

import store from './store'
import { increment, decrement, incrementByAmount } from './actions/counterActions'

// 获取当前状态
console.log(store.getState()) // { count: 0 }

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

// 分发 Action
store.dispatch(increment())        // { count: 1 }
store.dispatch(increment())        // { count: 2 }
store.dispatch(decrement())        // { count: 1 }
store.dispatch(incrementByAmount(5)) // { count: 6 }

// 取消订阅
unsubscribe()

使用工具函数 bindActionCreators

bindActionCreators 是 Redux 提供的工具函数,用于将 action creators 和 dispatch 绑定在一起,这样可以直接调用绑定的函数,而不需要每次都写 store.dispatch()

使用场景:

  • 简化 action creators 的调用
  • 在组件中传递已绑定的 action creators
  • connect 配合使用(详见下方"在 React 中使用"部分)
import { bindActionCreators } from 'redux'
import store from './store'
import { increment, decrement, incrementByAmount } from './actions/counterActions'

// 方式一:绑定单个 action creator
const boundIncrement = bindActionCreators(increment, store.dispatch)
boundIncrement()  // 等同于 store.dispatch(increment())

// 方式二:绑定多个 action creators(推荐)
const boundActions = bindActionCreators(
  {
    increment,
    decrement,
    incrementByAmount
  },
  store.dispatch
)

// 现在可以直接调用,无需再写 store.dispatch()
boundActions.increment()              // { count: 1 }
boundActions.increment()              // { count: 2 }
boundActions.decrement()              // { count: 1 }
boundActions.incrementByAmount(5)     // { count: 6 }

实现Redux

Redux 的设计模式和架构思想

**实现createStore

/**
 * 判断某个对象是否是一个 plain-object(纯对象)
 * @param {*} obj
 * @returns {boolean}
 */
function isPlainObject(obj) {
  if (typeof obj !== "object" || obj === null) {
    return false;
  }
  // 如果 obj 的原型是 Object.prototype,则返回 true
  return Object.getPrototypeOf(obj) === Object.prototype;
}
/**
 * 得到一个指定长度的随机字符串
 * @param {*} length
 * @returns {string}
 */
function getRandomString(length) {
  //36进制 数字10个+字母26个=36个字符 使用这种方式每个位置可以出现36种不同的字符
  return Math.random().toString(36).slice(2, 2 + length).split("").join("."); // slice(2, 2 + length) 从第3个位置开始截取 length 个字符
}


/**
 * 创建 Redux store
 * @param {*} reducer reducer 函数
 * @param {*} initialState 初始状态(可选,实际 Redux API 中第二个参数是 enhancer,这里简化实现)
 */
export default function (reducer, initialState) {
  // 当前使用的 reducer
  let currentReducer = reducer;
  // 当前仓库中的状态
  let currentState = initialState;
  // 记录所有的订阅者(监听器)
  const listeners = [];

  /**
   * 分发者
   * @param {*} action
   */
  function dispatch(action) {
    // 验证 action 是否是平面对象(通过 {} 或 new Object() 创建的对象,其原型链直接指向 Object.prototype,不继承其他类的实例。)
    if (!isPlainObject(action)) {
      throw new TypeError("action must be a plain object");
    }
    // 验证 action 的 type 属性是否存在
    if (action.type === undefined) {
      throw new TypeError("action must has a property of type");
    }
    //分发 action   调用 reducer 函数,得到新的状态
    currentState = currentReducer(currentState, action);
    // 运行数组中所有的订阅者(监听器)
    for (const listener of listeners) {
      listener();
    }
  }
  function getState() {
    // 返回当前仓库中的状态
    // 注意:在 dispatch 初始化 action 后,currentState 不会是 undefined
    return currentState;
  }

  /**
   * 订阅者
   * @param {*} listener
   * @returns
   */
  function subscribe(listener) {
    // 将监听器加入到数组中
    listeners.push(listener);
    let isRemove = false; //是否已经移除掉了
    // 返回一个取消订阅函数 这个函数需要执行,才能取消订阅
    return function () {
      if (isRemove) {
        return;
      }
      //拿到监听器在数组中的索引
      const index = listeners.indexOf(listener);
      //如果索引存在,则从数组中移除
      if (index !== -1) {
        listeners.splice(index, 1);
        //设置为已移除
        isRemove = true;
      }
    };
  }

  //创建仓库时,需要分发一次初始的action
  dispatch({
    type: `@@redux/INIT${getRandomString(6)}`,
  });

  return {
    dispatch,
    getState,
    subscribe,
  };
}
createStore 的设计模式和架构分析

555555.png

订阅机制

store.subscribe() 是一个全局订阅,无论哪个 reducer 更新,监听器都会被调用。在组件中由开发者自己提取需要的部分。

思想
1. 单向数据流架构

d x.png

  • 数据流向单一、可预测
  • 每个状态变化都有明确的路径
  • 便于调试和追踪
2. 不可变状态管理
设计模式协作关系

x z g x.png

**实现bindActionCreators

/**
 * 增强action创建函数的功能,使它不仅可以创建action,并且创建后会自动完成分发。
 * @param {*} actionCreators action创建函数(可以是单个函数或对象)
 * @param {*} dispatch 分发者
 */
export default function (actionCreators, dispatch) {
  // 情况1:单个 action creator(函数)
  if (typeof actionCreators === "function") {
    return function (...args) {
      const action = actionCreators(...args);
      dispatch(action);
    };
  }
  // 情况2:多个 action creators(对象)
  else if (typeof actionCreators === "object" && actionCreators !== null) {
    const result = {};
    for (const key in actionCreators) {
      // 判断是否是自己的属性
      if (actionCreators.hasOwnProperty(key)) {
        const actionCreator = actionCreators[key]; // 取出对应的属性值
        // 判断是否是函数
        if (typeof actionCreator === "function") {
          // 创建一个自动分发的action创建函数
          result[key] = function (...args) {
            const action = actionCreator(...args);
            dispatch(action);
          };
        }
      }
    }
    return result; //  添加返回值
  }
  // 情况3:接受的参数不是对象或函数,则抛出错误
  else {
    throw new TypeError(
      "actionCreators must be an object or function which means action creator"
    );
  }
}

**实现combineReducers

/**
 * 定义特殊的type值
 */
const ActionTypes = {
  INIT() {
    return `@@redux/INIT${getRandomString(6)}`;
  },
  UNKNOWN() {
    return `@@redux/PROBE_UNKNOWN_ACTION${getRandomString(6)}`;
  },
};
/**
 * 判断某个对象是否是一个 plain-object(纯对象)
 * @param {*} obj
 * @returns {boolean}
 */
function isPlainObject(obj) {
  if (typeof obj !== "object" || obj === null) {
    return false;
  }
  // 如果 obj 的原型是 Object.prototype,则返回 true
  return Object.getPrototypeOf(obj) === Object.prototype;
}
/**
 * 得到一个指定长度的随机字符串
 * @param {*} length
 * @returns {string}
 */
function getRandomString(length) {
  //36进制 数字10个+字母26个=36个字符 使用这种方式每个位置可以出现36种不同的字符
  return Math.random().toString(36).slice(2, 2 + length).split("").join("."); // slice(2, 2 + length) 从第3个位置开始截取 length 个字符
}

/**
 * 验证 reducer 函数
 * @param {*} reducers reducer 函数集合
 * @returns {void}
 */
function validateReducers(reducers) {
  //1. 验证 reducers 是否是对象
  if (typeof reducers !== "object") {
    throw new TypeError("reducers must be an object");
  }
  //2. 验证 reducers 是否是平面对象
  if (!isPlainObject(reducers)) {
    throw new TypeError("reducers must be a plain object");
  }
  //3. 验证 reducer 的返回结果是不是 undefined , 保证 reducer 函数在初始化时返回一个默认值
  for (const key in reducers) {
    //判断是否是自己的属性
    if (reducers.hasOwnProperty(key)) {
      const reducer = reducers[key]; //拿到reducer
      //传递一个特殊的type值
      let state = reducer(undefined, {
        type: ActionTypes.INIT(),
      });
      if (state === undefined) {
        throw new TypeError("reducer must not return undefined");
      }
      state = reducer(undefined, {
        type: ActionTypes.UNKNOWN(),
      });
      if (state === undefined) {
        throw new TypeError("reducer must not return undefined");
      }
    }
  }
}
/**
 * 组装 reducer 函数 并返回一个新reducer函数
 * @param {*} reducers reducer 函数集合 对象形式 对象的key是reducer的名称 对象的value是reducer函  数 例如:{loginUser:fn reducer,users:fn reducer}
 * @returns {function} 新 reducer 函数
 */
export default function (reducers) {
  validateReducers(reducers);
  return function (state = {}, action) {
    const newState = {}; //要返回的新的状态
    for (const key in reducers) {
      if (reducers.hasOwnProperty(key)) {
        const reducer = reducers[key];
        //旧的集合对象{loginUser:xxx,users:[xxx]}
        //旧的reducer函数集合{loginUser:fn reducer,users:fn reducer}
        //将旧的集合对象中的value传递给对应的同名reducer函数 并返回新的同名的value值
        newState[key] = reducer(state[key], action);
        //新状态{loginUser:xxx,users:[xxx]}  value 是 每一个reducer的返回值
      }
    }
    return newState; //返回新的集合对象
  };
}

为什么要验证两次?

确保 reducer 必须返回有效值,避免返回 undefined

  1. INIT 测试:确保 reducer 在初始化时(state 为 undefined)能返回有效状态
  2. UNKNOWN 测试:确保 reducer 在处理未知 action 时不会返回 undefined

为什么要测试 INIT?

createStore 创建 store 时,会调用一次 reducer 来初始化状态 如果 reducer 在 stateundefined 时返回 undefined,store 的初始状态就是 undefined,这会导致应用崩溃。

// ✅ 正确的 reducer
function goodReducer(state = { count: 0 }, action) {
  // 有默认参数,state 为 undefined 时使用 { count: 0 }
  return state;
}
// 测试:goodReducer(undefined, { type: '@@redux/INIT...' })
// 结果:返回 { count: 0 } ✅ 通过

// ❌ 错误的 reducer
function badReducer(state, action) {
  // 没有默认参数,也没有处理 undefined 的情况
  return state; // state 是 undefined,返回 undefined
}

// 测试:badReducer(undefined, { type: '@@redux/INIT...' })
// 结果:返回 undefined ❌ 验证失败,抛出错误

为什么要测试 UNKNOWN?

在实际使用中,可能会 dispatch 一个 reducer 不认识的 action。reducer 必须能够处理这种情况,返回当前状态(或默认状态),而不是返回 undefined

// ✅ 正确的 reducer
function goodReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    default:
      return state; // 处理未知 action,返回当前状态
  }
}

// 测试:goodReducer(undefined, { type: '@@redux/PROBE_UNKNOWN_ACTION...' })
// 结果:返回 { count: 0 } ✅ 通过(default 分支返回默认状态)

// ❌ 错误的 reducer
function badReducer(state = { count: 0 }, action) {
  if (action.type === "INCREMENT") {
    return { ...state, count: state.count + 1 };
  }
  // 问题:没有 else 或 default,当 action.type 不匹配时,函数返回 undefined
}

// 测试:badReducer(undefined, { type: '@@redux/PROBE_UNKNOWN_ACTION...' })
// 结果:返回 undefined ❌ 验证失败,抛出错误

验证会在以下情况发现错误:

  1. 没有默认参数function reducer(state, action) 而不是 function reducer(state = {}, action)
  2. 没有 default 分支:switch 语句或 if 语句没有处理所有情况
  3. 返回 undefined:某些情况下函数没有返回值

Redux中间件(Middleware)

在 Redux 中,中间件主要用于增强 dispatch 函数

原理:更改仓库中的 dispatch 函数,在原有dispatch的基础上自定义逻辑

let oldDispatch = store.dispatch//保留dispatch的引用
store.dispatch = function(action){
  //在这里面可以自定义逻辑
  oldDispatch(action)//调用原本的dispatch
}

oldDispatch = store.dispatch 
store.dispatch  = function(action){
  //可以再次自定义逻辑
  oldDispatch(action)//调用已经增强过的的dispatch
}
常见应用场景
  • 日志打印(redux-logger)
  • 处理异步(redux-thunk / redux-saga)
  • 接口请求
  • 权限判断
  • 崩溃处理
  • 重试机制

规范写法

  • 其中的applyMiddleware 函数,用于记录有哪些中间件,它会返回一个函数

    • 该函数用于记录创建仓库的方法,然后又返回一个函数
// 异步action处理中间件
function thunk(store) {
    return function (next) {
        //下面返回的函数,是最终要应用的dispatch
        return function (action) {
            // 如果action是函数,则执行它并传入dispatch和getState
            if (typeof action === 'function') {
                return action(store.dispatch, store.getState);
            }
            // 否则,正常传递给下一个中间件
            return next(action);
        }
    }
}

// 错误捕获中间件
function errorHandler(store) {
    return function (next) {
        //下面返回的函数,是最终要应用的dispatch
        return function (action) {
            try {
                return next(action);
            } catch (error) {
                console.error('Redux错误:', error);
                // 可以dispatch一个错误action
                store.dispatch({ type: 'ERROR', payload: error.message });
                throw error;
            }
        }
    }
}

// 应用中间件:使用 applyMiddleware 函数
import { createStore, applyMiddleware } from 'redux';

// 方法一: 创建 store 时应用中间件
const store = createStore(
    reducer,                    // reducer 函数
    applyMiddleware(            // 应用中间件
        thunk,                 // 异步处理中间件
        errorHandler           // 错误捕获中间件
    )
);
//方法二: 参数:第一层函数传递中间件  第二层函数传递创建仓库的方法 第三层函数传递创建仓库时具体的参数
const store = applyMiddleware(thunk, errorHandler)(createStore)(reducer)

// 多个中间件会按照传入的顺序依次执行
// 执行顺序:thunk -> errorHandler -> 原始 dispatch

关键点:

  • next(action)暂停当前中间件的执行,进入下一个中间件(函数调用栈加深)
  • 当下一层执行完毕返回时,当前中间件从 next(action) 之后继续执行(函数调用栈回退)
  • 这就是"洋葱模型":先一层层进入,再一层层返回 ![image-20251222211841123](/Users/wangbo/Library/Application Support/typora-user-images/image-20251222211841123.png)

第三方中间件

异步操作处理 - Redux-Thunk

Redux-Thunk 允许编写返回函数而不是动作对象的 action creators,实现异步操作。

注意:如果使用 Redux Toolkitredux-thunk 已经内置在 configureStore 中,无需手动安装和配置。

安装(仅传统 Redux 需要)
npm install redux-thunk
配置(仅传统 Redux 需要)
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
)
编写异步 Action
// 异步 Action
const fetchUserData = (userId) => {
  return async (dispatch, getState) => {
    dispatch({ type: 'FETCH_USER_REQUEST' })
    try {
      const response = await fetch(`https://api.example.com/user/${userId}`)
      const data = await response.json()
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: data })
    } catch (error) {
      dispatch({ type: 'FETCH_USER_FAILURE', error })
    }
  }
}

// 调用
store.dispatch(fetchUserData(1))

Redux-Logger

Redux-Logger 用于记录 Redux 状态变化,帮助开发者调试应用程序。

安装
npm install redux-logger
配置
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import rootReducer from './reducers/index'

const store = createStore(
  rootReducer,
  applyMiddleware(logger)
)

React18+ --> Redux Hooks + Redux Toolkit

目前 Redux 官方强制推荐使用 Redux Toolkit(RTK) 来管理 Redux 状态,而 RTK 的configureStore已经内置了中间件的处理逻辑,无需手动调用applyMiddleware

工具 / 库核心职责
Redux Toolkit(RTK)负责Redux store 的创建、reducer/action 的定义、异步逻辑(如 createAsyncThunk) 等底层状态管理逻辑,简化 Redux 的样板代码。
React-Redux Hooks作为React 和 Redux 之间的桥梁,让 React 组件能通过 Hooks 轻松访问 Redux store 的状态和 dispatch 方法(核心 Hooks:useSelectoruseDispatchuseStore)。

Redux Toolkit

Redux 官方推荐的工具集,可简化 Redux 的使用

  • configureStore:简化 Store 的创建和配置,自动设置 Redux DevTools 和常用中间件
  • createSlice:简化 Reducer 和 Action Creator 的创建,自动生成 Action 类型和 Creator 函数
  • createReducer:使用 Immer 简化 Reducer 的编写,可以直接修改状态
  • createAction:创建标准的 Action Creator 函数
  • createAsyncThunk:处理异步操作,自动生成 pending、fulfilled、rejected 三种状态的 Action
  • createEntityAdapter:简化规范化状态的管理(如列表数据的增删改查)
  • 内置 Immer:允许使用 "mutable" 语法编写不可变的更新逻辑
  • 内置 Redux-Thunk:默认支持异步操作,无需额外配置
  • 自动配置 Redux DevTools:开发环境下自动启用调试工具
安装
npm install @reduxjs/toolkit react-redux
创建 Store
// app/store.js
import { configureStore } from '@reduxjs/toolkit'

export default configureStore({
  reducer: {}
})
创建 Slice

Slice 的作用:将ReducerAction CreatorAction 类型组合在一起,形成一个完整的状态管理单元

Slice 包含三个部分:

  1. name:Slice 的名称,用于生成 Action 类型的前缀(如 counter/increment
  2. initialState:该 Slice 的初始状态
  3. reducers:定义状态更新逻辑的对象,每个 reducer 函数对应一个 Action

Slice 自动生成的内容:

  • Action Creator:每个 reducer 函数会自动生成对应的 Action Creator(如 increment()decrement()
  • Action 类型:自动生成 Action 类型字符串(如 'counter/increment'
  • Reducer 函数:将所有 reducers 合并成一个 reducer 函数
// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',  // Slice 名称,用于生成 Action 类型前缀
  initialState: {   // 初始状态
    value: 0
  },
  reducers: {      // 定义状态更新逻辑
    increment: state => {
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

// 导出自动生成的 Action Creator
export const { increment, decrement, incrementByAmount } = counterSlice.actions

// 导出 Reducer
export default counterSlice.reducer

createSlice 的返回值结构:

createSlice 函数会返回一个对象,包含以下属性:

  1. actions:一个对象,包含所有自动生成的 Action Creator 函数

    // counterSlice.actions 的结构类似:
    {
      increment: () => ({ type: 'counter/increment' }),
      decrement: () => ({ type: 'counter/decrement' }),
      incrementByAmount: (amount) => ({ 
        type: 'counter/incrementByAmount', 
        payload: amount 
      })
    }
    

    可以通过 counterSlice.actions.increment 访问,或者解构:

    const { increment, decrement } = counterSlice.actions
    
  2. reducer:一个合并后的 Reducer 函数

    // counterSlice.reducer 是一个函数,类似:
    function counterReducer(state = initialState, action) {
      switch (action.type) {
        case 'counter/increment':
          return { ...state, value: state.value + 1 }
        case 'counter/decrement':
          return { ...state, value: state.value - 1 }
        case 'counter/incrementByAmount':
          return { ...state, value: state.value + action.payload }
        default:
          return state
      }
    }
    

    这个 reducer 是由 createSlice 根据你定义的 reducers 对象自动生成的。

工作原理:

  1. 你定义了 reducers 对象中的函数(如 incrementdecrement

  2. createSlice 自动为每个 reducer 函数:

    • 生成对应的 Action 类型(如 'counter/increment'
    • 生成对应的 Action Creator(如 increment()
    • 将这些 Action Creator 放入返回对象的 actions 属性中
  3. createSlice 将所有 reducer 函数合并成一个 reducer,放入返回对象的 reducer 属性中

使用示例:

// 在组件中使用
import { useDispatch } from 'react-redux'
import { increment, decrement } from './counterSlice'

function Counter() {
  const dispatch = useDispatch()
  
  // increment() 会自动生成 { type: 'counter/increment' } 这样的 Action
  return <button onClick={() => dispatch(increment())}>+</button>
}
将 Reducer 添加到 Store
// app/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})
Immer 数据更新机制

Redux Toolkit 已经内置了 Immer,可以直接使用 "mutable 语法" 写 reducer: 原理:在 RTK 的createSlice里写 reducer 时,Immer 会将草稿(通过proxy代理创建出基于原state的代理对象)自动处理成全新的不可变对象(未修改的部分会直接复用原状态的引用,提升性能)。

import { createSlice } from "@reduxjs/toolkit"

const todoSlice = createSlice({
  name: "todos",
  initialState: [],
  reducers: {
    toggle(state, action) {
      const todo = state.find(t => t.id === action.payload)
      if (todo) todo.done = !todo.done  // 可以直接修改
    },
    add(state, action) {
      state.push(action.payload)  // 可以直接 push
    }
  }
})

注意:只能在 Redux Toolkit 的 createSlicecreateReducer 中编写 "mutation" 逻辑,因为它们在内部使用 Immer。如果在没有 Immer 的 reducer 中编写 mutation 逻辑,将改变状态并导致错误。

React-Redux

React-Redux:连接 React 和 Redux 的官方绑定库

  • 提供 Provider 组件:将 Redux store 注入到 React 应用
  • 提供 connect 高阶组件:连接 React 组件和 Redux store(传统方式)
  • 提供 Hooks API:useSelectoruseDispatch 等(现代推荐方式)
  • 注意connect 不是用于连接 Redux 和其他库的,它专门用于连接 React 组件和 Redux store
安装 React-Redux
npm install react-redux
使用 Provider 包裹应用
// index.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import store from './store'
import App from './App'

const root = createRoot(document.getElementById('root'))
root.render(
  <Provider store={store}>
    <App />
  </Provider>
)
使用 connect 连接组件
  • Redux 本身是框架无关的,可以在任何 JavaScript 应用中使用,而connectReact-Redux 提供的高阶组件,专门用于连接 React 组件Redux store

connect 的作用:

  1. 将 Redux store 的 state 映射到 React 组件的 props
  2. 将 dispatch 方法和 action creators 映射到 React 组件的 props
  3. 自动订阅 store 的变化,当 state 更新时重新渲染组件
  4. 优化性能,只在相关 state 变化时才重新渲染组件

基本用法:

// reducers/counterReducer.js
const initialState = {
  count: 0
}

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 }
    case 'DECREMENT':
      return { ...state, count: state.count - 1 }
    case 'INCREMENT_BY_AMOUNT':
      return { ...state, count: state.count + action.payload }
    default:
      return state
  }
}

export default counterReducer


// components/Counter.js
import React from 'react'
// 导入 connect 高阶组件,用于连接 React 组件和 Redux store
import { connect } from 'react-redux'
// 导入 action creators,这些函数用于创建 action 对象
import { increment, decrement, incrementByAmount } from '../actions/counterActions'

// Counter 组件:接收从 Redux store 映射过来的 props
// count: 从 store 中获取的计数状态
// increment, decrement, incrementByAmount: 已绑定 dispatch 的 action creators
function Counter({ count, increment, decrement, incrementByAmount }) {
  return (
    <div>
      {/* 点击按钮时直接调用 increment,无需手动 dispatch */}
      <button onClick={increment}>+</button>
      {/* 显示当前计数值 */}
      <span>{count}</span>
      {/* 点击按钮时直接调用 decrement */}
      <button onClick={decrement}>-</button>
      {/* 点击按钮时调用 incrementByAmount,传入参数 5 */}
      <button onClick={() => incrementByAmount(5)}>+5</button>
    </div>
  )
}

// mapStateToProps: 将 Redux store 的 state 映射到组件的 props
// 参数 state 是 Redux store 的完整状态树
// 返回一个对象,对象的属性会成为组件的 props
const mapStateToProps = (state) => ({
  count: state.count  // 从 state 中提取 count 属性,作为组件的 count prop
})

// mapDispatchToProps: 将 dispatch 方法映射到组件的 props(方式一:直接传递对象,推荐)
// 当传递一个对象时,connect 会自动使用 bindActionCreators 绑定 dispatch
// 对象中的每个 action creator 都会被包装,使其在调用时自动 dispatch
const mapDispatchToProps = {
  increment,           // 等同于: increment: increment
  decrement,           // 等同于: decrement: decrement
  incrementByAmount   // 等同于: incrementByAmount: incrementByAmount
}

// connect 高阶组件:连接组件和 Redux store
// 第一个参数 mapStateToProps: 定义如何从 store 中获取数据
// 第二个参数 mapDispatchToProps: 定义如何将 action creators 绑定到组件
// 返回一个增强后的组件,该组件会自动接收映射的 props
export default connect(mapStateToProps, mapDispatchToProps)(Counter)

使用 工具函数bindActionCreators :

在使用 connect 时,如果 mapDispatchToProps 是一个对象,connect 内部会自动使用 bindActionCreators。你也可以手动使用 bindActionCreators

// components/Counter.js
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { increment, decrement, incrementByAmount } from '../actions/counterActions'

function Counter({ count, increment, decrement, incrementByAmount }) {
  return (
    <div>
      <button onClick={increment}>+</button>
      <span>{count}</span>
      <button onClick={decrement}>-</button>
      <button onClick={() => incrementByAmount(5)}>+5</button>
    </div>
  )
}

const mapStateToProps = (state) => ({
  count: state.count
})

// 方式二:手动使用 bindActionCreators
const mapDispatchToProps = (dispatch) => {
  return bindActionCreators(
    {
      increment,
      decrement,
      incrementByAmount
    },
    dispatch
  )
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter)

注意: 以上两种 mapDispatchToProps 的写法是等价的。方式一(直接传递对象)更简洁,是推荐写法,因为 connect 内部会自动使用 bindActionCreators

使用 React-Redux Hooks

Hooks 与 connect 的关系:

React-Redux 提供了 Hooks API(useSelectoruseDispatch),可以完全替代 connectbindActionCreators,使我们不需要手动使用 bindActionCreators,直接使用 dispatch 即可。

对比:

方式获取状态分发 Action特点
connect通过 mapStateToProps通过 mapDispatchToPropsbindActionCreators高阶组件模式,需要包装组件
Hooks使用 useSelector使用 useDispatch函数式组件,代码更简洁,推荐使用

注意: connect 仍然可以使用,特别是在类组件中,但函数组件推荐使用 Hooks。

useSelector - 获取状态

useSelector 用于从 Redux store 中获取状态。它接受一个选择器函数,该函数接收整个 state 作为参数,可以指定返回组件需要的部分状态。

注意: state 的结构取决于你如何组织 reducer:

  • 如果使用 combineReducers,state 结构为 { counter: {...}, todos: {...} }
  • 如果只有一个 reducer,state 结构就是 reducer 返回的状态对象
import { useSelector } from 'react-redux'

// 示例1:使用 combineReducers 后的 state 结构
function Counter() {
  const count = useSelector(state => state.counter.count)  // 或 state.counter.value(取决于 reducer)
  return <div>{count}</div>
}

// 示例2:只有一个 reducer 的 state 结构
function Counter() {
  const count = useSelector(state => state.count)  // 直接访问 count
  return <div>{count}</div>
}
useDispatch - 分发 Action

useDispatch 返回 store 的 dispatch 方法,用于分发 action。

import { useDispatch } from 'react-redux'
import { increment, decrement } from '../actions/counterActions'  // 传统 Redux 的 action creators

function Counter() {
  const dispatch = useDispatch()
  
  return (
    <div>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  )
}

注意: 如果使用 Redux Toolkit,可以导入从 createSlice 生成的 action creators:

import { increment, decrement } from './counterSlice'  // Redux Toolkit 的 action creators
完整示例

Hooks 替代 connect 和 bindActionCreators 的对比:

// ========== 使用 connect + bindActionCreators(传统方式)==========
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { increment, decrement, incrementByAmount } from '../actions/counterActions'

function Counter({ count, increment, decrement, incrementByAmount }) {
  return (
    <div>
      <button onClick={increment}>+</button>
      <span>{count}</span>
      <button onClick={decrement}>-</button>
      <button onClick={() => incrementByAmount(5)}>+5</button>
    </div>
  )
}

const mapStateToProps = (state) => ({ count: state.count })
const mapDispatchToProps = (dispatch) => 
  bindActionCreators({ increment, decrement, incrementByAmount }, dispatch)

export default connect(mapStateToProps, mapDispatchToProps)(Counter)

// ========== 使用 Hooks(现代方式,推荐)==========
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, incrementByAmount } from '../actions/counterActions'

function Counter() {
  const count = useSelector(state => state.count)  // 替代 mapStateToProps
  const dispatch = useDispatch()                     // 替代 mapDispatchToProps + bindActionCreators

  return (
    <div>
      <button onClick={() => dispatch(increment())}>+</button>
      <span>{count}</span>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
    </div>
  )
}

export default Counter

总结:

  • useSelector 替代了 mapStateToPropsconnect 的状态映射功能
  • useDispatch 替代了 mapDispatchToPropsbindActionCreators 的功能
  • 不需要高阶组件包装,代码更简洁直观

性能优化提示:

useSelector 使用严格相等(===)来比较前后两次选择器返回的值。如果值相同,组件不会重新渲染。

// 每次都会创建新对象,导致不必要的重新渲染
const data = useSelector(state => ({ count: state.count }))  // ❌ 不推荐

// 直接返回原始值,只有 count 变化时才重新渲染
const count = useSelector(state => state.count)  // ✅ 推荐

// 如果需要返回多个值,使用多个 useSelector
const count = useSelector(state => state.count)
const todos = useSelector(state => state.todos)  // ✅ 推荐

// 或者使用浅比较(需要额外配置)
import { shallowEqual } from 'react-redux'
const { count, todos } = useSelector(state => ({ 
  count: state.count, 
  todos: state.todos 
}), shallowEqual)  // ✅ 使用浅比较