普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月10日首页

一文梳理Redux及衍生状态管理库 [共4000字-阅读时长20min]

2025年12月10日 00:07

Redux的基本组成

  1. state,所有的状态都以对象树的方式存在单一的store中。
  1. action,唯一修改state就是创建action,通过dispatch将action传递给reducer进行修改state。
  1. reducer,reducer是一个纯函数,它有两个参数一个是旧state,一个是新的action,函数的返回值是新的state。新的state被计算之后,state会自动传递给所有注册了监听器的组件,从而触发了重新渲染。

下图就是redux整体的工作流程图。

如果现在问redux的基本原理是什么,描述一下redux的工作流程,就可以这样回答。

redux采用单一数据源的方式来管理状态数据,state都是只读状态不可修改,唯一修改状态的方式就是通过dispatch传递一个action给reducer,reducer返回一个新的状态,这也是redux的工作流程。

Flux架构

Flux将一个应用分成四个部分,Redux就是flux架构。

  • View: 视图层
  • Action(动作):视图层发出的消息(比如mouseClick)
  • Dispatcher(派发器):用来接收Actions、执行回调函数
  • Store(数据层):用来存放应用的状态,一旦发生变动,就提醒Views要更新页面

为什么Redux要采用Flux架构?

  1. 职责分离,action描述更新的内容,reducer描述怎么更新,state则描述管理状态。

  2. 构建出单向数据流,这种单向数据流让状态变化变得可预测、可调试

Redux具体使用

第一步,需要通过createStore创建一个redux实例,并且需要编写很多的样板代码。

// store.js
import { createStore } from 'redux';

// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// Reducer
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    default:
      return state;
  }
}

// Create Store
export const store = createStore(counterReducer);

第二步,通过context将redux实例注入给整个组件树。

import { StrictMode, createContext } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import { store } from './store/index.js'// 引用redux实例

export const ReduxContext = createContext()

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <ReduxContext.Provider value={{ store, dispatch: store.dispatch }}>//通过context将实例注入给整个组件树
      <App />
    </ReduxContext.Provider>
  </StrictMode>,
)

第三步,函数组件通过useContext,类组件通过XXXContext.Consumer进行消费。获取到对应的store_statedispatch。并且需要手动进行注册Redux监听器,这样当Redux的state发生变化的时候,组件可以监听到并进行重新渲染。

import { useContext, useEffect, useState } from "react";
import { ReduxContext } from "./main";

const Comp1 = () => {
    const {store, dispatch} = useContext(ReduxContext)
    const [currentState, setCurrentState] = useState(store.getState())

    useEffect(() => {
    // 订阅Redux的state的变化
        const unsubscribe = store.subscribe(() => {
        // 当redux state 变化的时候,调用setState触发组件的rerender更新UI
            setCurrentState(store.getState())
        })
        return () => {
            unsubscribe()
        }
    },[store])

    return <div>
        <h1>{currentState.count}</h1>
        <button onClick={() => dispatch({type:"INCREMENT"})}>+</button>
        <button onClick={() => dispatch({type:"DECREMENT"})}>-</button>
    </div>
}

export default Comp1;

 Redux不依赖于框架,想要实现Redux的state变化驱动UI更新的话,就要构建一个UI和  Redux  的绑定层(React-Redux),该层是依赖于  Redux  的发布订阅模式,可以通过 store.subscribe订阅Reduxstate变化,并在回调函数中出发组件的setState,实现UI的更新。这就React-Redux框架的作用。

什么是React-Redux?

React-redux是React和Redux的绑定库,它充当了数据层和视图层的桥梁,通过订阅Store的数据变化,自动触发组件的re-render更新视图,让Store的数据变化useState可以达到一致的响应式方式。

React-Redux的组成:

  1. Provider组件

在React项目中,可以通过  Provider  组件,将创建的store对象注入到整个组件树中,任何想要使用Redux Store的子组件都可以通过useSelectorconnect来访问。

  1. 简约使用,不用一层一层的传递props
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import store from './store';
import { Provider } from 'react-redux';

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(
    <Provider store={store}>
      <App />
    </Provider>
)
  1. useSelector,useDispatch钩子函数

useSelector的作用是从Redux Store中订阅读取需要的状态数据,并且这些状态都是响应式的,状态变化的时候会触发组件rerenderuseDispatch用来获取React-reduxdispatch函数,派发action修改Redux Store的状态。

  1. 响应式数据,避免了繁琐的订阅数据的代码,使用useSelector天然就是响应式的。

  2. hooks和函数式组件适配程度高


  1. connect函数

connect一般是和类组件搭配使用,需要定义mapStateToProps 和mapDispatchToProps 通过connect将Redux和组件连接起来。

import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions';

function App({ count, increment, decrement }) {
  return (
    <div>
      <button onClick={increment}> + </button>
      <p>{count}</p>
      <button onClick={decrement}> - </button>
    </div>
  )
}

const mapStateToProps = state => {
  return {
    count: state.count,
  }
}

const mapDispatchToProps = {
  increment,
  decrement,
}

// 使用 connect 函数连接组件和 Redux store
export default connect(mapStateToProps, mapDispatchToProps)(App)

Redux辅助函数&中间件

combineReducers(肯拜n 瑞丢瑟斯)

目前的reducer处理的整体项目的Redux Store,既处理counter,又处理home。所有的状态都放在一个reducer中管理,必然会造成代码的臃肿和难以维护。

根据这种情况可以对reducer进行逻辑的拆分,counter类的逻辑抽出一个reducer,home类的逻辑抽出一个reducer。在reducers拆分的同时,实际上对Redux state也进行了拆分。

combineReducers 插件则是将抽象的reducer整合起来。

import { combineReducers } from 'redux';

// 1. 按业务域拆分成多个小的 reducer
const userReducer = (state = {}, action) => {
  switch(action.type) {
    case 'USER_LOGIN': return { ...state, ...action.payload };
    default: return state;
  }
};

const cartReducer = (state = {}, action) => {
  switch(action.type) {
    case 'ADD_TO_CART': return { ...state, items: [...state.items, action.item] };
    default: return state;
  }
};

const productsReducer = (state = [], action) => {
  switch(action.type) {
    case 'FETCH_PRODUCTS': return action.products;
    default: return state;
  }
};

// 2. combineReducers 建立 key 到 reducer 的映射关系
const rootReducer = combineReducers({
  user: userReducer,       // key "user" 映射到 userReducer
  cart: cartReducer,       // key "cart" 映射到 cartReducer  
  products: productsReducer // key "products" 映射到 productsReducer
});

// 这是上述拆分后的Redux Store
{
  user: {...},      // 由 userReducer 管理
  cart: {...},      // 由 cartReducer 管理  
  products: [...]   // 由 productsReducer 管理
}

combineReducers的实现原理

export const combineReducers = function (reducers) {
  // reducers = {count: setCount}
  const keys = Object.keys(reducers); // ['count']
  // 返回根Reducers,第一个参数是state,第二个参数是action
  return function (state = {}, action) {
    const nextState = {};
    keys.forEach((key) => {
      const reducer = reducers[key];
      const prev = state[key]; // { count: 0 }
      const next = reducer(prev, action); // 0 + 1 || 0 - 1
      nextState[key] = next;
    });
    return nextState;
  };
};

1.combineReducers返回一个根Reducers,第一个参数是state,第二个参数是action

2.经过combineReducers的处理,使用dispatch传递action对象和没有使用combineReducers处理的时候是一样的。不会发生处理之后actionstype就会从ADD_TO_CART,变为cart/ADD_TO_CARTcombineReducers本质是将reducers物理分离管理(代码分离,不写在一起),对应的state逻辑分离管理

3.根Reducers接收到action的时候,会遍历所有的子Reducers,将actions传递进去,获得新的state值。

4.将所有的新的state值进行汇总然后返回。

还记得吗?Redux修改state的方法就是通过reducers返回新的值,来更新Store数据的。

中间件

Redux中间件采用AOP思想,在actionreducer的链路中插入切面逻辑,扩展自己的逻辑。

中间件的核心:增强dispatch的能力,让action到达reducers 之前可以进行一些操作处理。

洋葱模型

这是理解中间件执行顺序最关键的模型。Koa 的中间件也是这个原理。

想象你有一个洋葱,最核心(最里面)的是真实的store.dispatch(也就是派发给 Reducer) 。每一层洋葱皮就是一个中间件

当你发起一个action时:

  1. 进场(In-bound): 就像一根针穿过洋葱,你需要先穿透最外层的皮(中间件1),再穿透第二层(中间件2)……直到刺中核心(Reducer)。
  1. 核心处理: Reducer 接收 action,更新 state。
  1. 出场(Out-bound): 针刺中核心后,如果函数有返回值或者有后续逻辑,代码执行权会沿着原路退出来:从核心 -> 中间件N -> ... -> 中间件1。

3. 代码签名的秘密: store => next => action 

你在源码中看到的中间件通常长这样,这叫柯里化(Currying) 函数:

const logger = store => next => action => {
  console.log('1. [Logger] 准备派发:', action);
  // 这句 next(action) 是关键!
  // 它代表:把控制权交给“下一层洋葱皮”
  let result = next(action); 
  console.log('2. [Logger] 派发完成,State更新了');
  return result;
}

让我把这三个箭头拆解开,你就懂了:

  1. 最外层  store :这是为了让中间件能获取状态 ( getState ) 或再次派发 ( dispatch )。
  1. 中间层  next :这是灵魂!
  •  next  并不总是指真实的 store.dispatch。
  • 它指的是链条中的下一个中间件
  • 如果是最后一个中间件,它的next才是真的store.dispatch
  • 如果你在中间件里不调用next(action),请求就会在这里断掉,永远到不了Reducer。
  1. 最内层  action :这就是你平时写的  dispatch({  type: 'ADD'})  里的那个对象。

4. 执行顺序演示(模拟大脑运行)

假设我们有两个中间件: M1  和  M2 。顺序是  applyMiddl  eware(M1, M2) 。

代码逻辑如下:

  • M1:  console.lo  g('M1 Start')  ->  next(actio  n)  ->  console.lo  g('M1 End') 
  • M2:  console.lo  g('M2 Start')  ->  next(actio  n)  ->  console.lo  g('M2 End') 

执行流程是这样的:

  1. 用户调用dispatch(action) 。
  1. 进入 M1
  • 打印  'M1 Start' 
  • 遇到  next(action)  -> 暂停 M1,进入 M2(因为 M1 的 next 就是 M2)。
  1. 进入 M2
  • 打印  'M2 Start' 
  • 遇到  next(action)  -> 暂停 M2,进入 Reducer(因为 M2 是最后一个,它的 next 是真 dispatch)。
  1. Reducer 执行,Store 数据更新。
  1. 回溯(洋葱出场)
  • Reducer 执行完,M2 的  next  函数返回。
  • 打印  'M2 End' 。
  • M2 函数执行结束,返回到 M1。
  • M1 的  next  函数返回。
  • 打印  'M1 End' 。

最终控制台输出:

M1 Start
M2 Start
  (Reducer 更新 State)
M2 End
M1 End

总结执行顺序:

  • 前半部分逻辑:正序 (M1 -> M2)
  • 后半部分逻辑(next 之后):倒序 (M2 -> M1)

5. 极简版源码实现(5行代码看懂原理)

Redux 的  compose  和  applyMiddl  eware  看起来很吓人,但如果去掉所有类型检查和边缘情况,它的核心逻辑可以用  Array.prot  otype.reduce  来解释。

假设我们有一堆中间件  [m1, m2, m  3]  和真实的  dispatch 。

我们需要把它们组装成: m1(m2(m3(d  ispatch))) 。

// 这是一个简化的 compose 实现
// 目的:把 [fn1, fn2, fn3] 变成 fn1(fn2(fn3(...args)))
const chain = middlewares.map(middleware => middleware(store));


// 这里的 dispatch 不再是原来的 dispatch,而是包装后的
// 这里的 reduce 就是要把中间件连起来
// 第一次:next = realDispatch,  item = m3 -> 返回 m3(realDispatch)
// 第二次:next = m3(realDispatch), item = m2 -> 返回 m2(m3(realDispatch))
// ...
const newDispatch = chain.reduceRight((next, item) => item(next), store.dispatch);

现在分段解释代码:

第一段代码

 const chain = middlewares.map(middleware => middleware(store)); 

首先要理解这段代码,必须要先明确  Redux  规定的中间件的格式是什么样子的,上文已经提到过了中间件签名

const myMiddleware = state => next => action => {
  //中间件代码
}

向上述的中间件传参  store  ,就可以得到如下函数:

(next) => (action) => {
  //中间件代码
}

这就相当于拨掉了洋葱的第一层,剩下的这个函数就很适合通过next将中间件进行连接了。

第二段代码

 const newDispatch = chain.reduceRight((next, item) ⇒ item(next), store.dispatch) 

首先需要明确的是 reduceRight  是在数组上从右向左遍历,原因就是中间件是对dispatch的加强,要将原始store.dispatch放在洋葱中心最后调用,然后一层一层的包裹上一次遍历处理的就是下一次的参数

 next指的是下一个中间件或者原生dispatch

多个中间件的二层函数会变为一个嵌套多层的newDispatch

(next) => (action) => {
  //中间件代码
}
(next) => (action) => {
  //中间件代码
}
(next) => (action) => {
  //中间件代码
}

变为:JavaScript

(action) => {
  // next
  (action) => {
    //next
    (action) => {
    //中间件代码
    }
  }
}


M1( M2( M3( realDispatch ) ) )

现在,当你调用newDispatch时,你实际上是在调用  m1 , m1  里的  next  引用着  m2 ,以此类推。使用dispatch,实际上已经经过多层的中间件加强后,才会调用真正的dispatch

Redux Thunk,为什么需要 Thunk(桑克)?

作用:让 Redux 的  dispatch  方法除了能接收“普通对象”外,还能接收“函数”,从而在函数内部处理异步逻辑。总结增加dispatch处理异步逻辑的能力。

在不使用Redux Thunk的时候,action creator必须返回一个对象,比如:

// 同步 action - 正常工作
const increment = () => {
  return {
    type: 'INCREMENT'
  };
};
dispatch(increment()); // ✅ 正常工作

如果increment函数是一个异步操作的会报错。

// 异步 action - 会报错!
const fetchUser = () => {
  setTimeout(() => {
    return {
      type: 'FETCH_USER_SUCCESS',
      payload: userData
    };
  }, 1000);
};

dispatch(fetchUser()); // ❌ 错误!返回的是 undefined

Redux Thunk 的解决方案

 redux-thunk 是如何做到让我们可以发送异步的请求呢?

  • 默认情况下的dispatch(action) action需要是一个 JavaScript 的对象
  •  redux-thunk 可以让dispatch( action 函数),  action 可以是一个函数
  • 该函数会被调用, 并且会传给这个函数两个参数: 一个dispatch函数和getState函数
  •  dispatch函数用于我们之后再次派发action 
  •  getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态 Thunk 中间件允许action creator 返回函数而不是 action 对象
// 使用 Thunk 后的异步 action
const fetchUser = (userId) => {
  // 返回一个函数,而不是对象
  return async (dispatch, getState) => {
    try {
      // 开始加载
      dispatch({ type: 'FETCH_USER_START' });
      // 执行异步操作
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      // 成功
      dispatch({ 
        type: 'FETCH_USER_SUCCESS', 
        payload: userData 
      });
    } catch (error) {
      // 失败
      dispatch({ 
        type: 'FETCH_USER_FAILURE', 
        payload: error.message 
      });
    }
  };
};
// 现在可以正常 dispatch 异步 action 了
dispatch(fetchUser(123)); // ✅ 正常工作!

 React-sage(sei只) :用于解决redux异步编程的中间件,React-sage则是通过generator来解决异步问题。

React-logger:在控制台打印出每一次Action触发前后的State变化。

Redux的实现原理

总结:Redux是一个基于发布-订阅模式的状态容器,通过单向数据流的方式来管理应用状态。

export const createStore = function (reducer, initState) {
  let listeners = [];
  let state = initState;
  function subscribe(listener) {
    // 我们希望,订阅了数据的handler,在数据改变时,都能执行。
    listeners.push(listener);
  }

  function dispatch(action) {
    // 单向数据流,而不是双向绑定。
    const newState = reducer(state, action); // 触发combineReducers的闭包
    state = newState;
    listeners.forEach((fn) => fn());
  }

  dispatch({ type: Symbol() });

  // 如果别人想要获取数据?
  function getState() {
    return state;
  }

  return {
    getState,
    subscribe,
    dispatch,
  };
};

为什么要推出Redux Toolkit?

或者说原生Redux有什么缺点?

  1. 过多的样板代码

修改一个变量,要在  constants.  js (定义常量)、 actions.js (写创建函数)、 reducers.js (写 switch-case)三个文件里反复横跳。

// constants.js
export const INCREMENT = 'counter/INCREMENT';
export const DECREMENT = 'counter/DECREMENT';
export const SET_VALUE = 'counter/SET_VALUE';

// actions.js
import { INCREMENT, DECREMENT, SET_VALUE } from './constants';


export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });
export const setValue = (val) => ({ type: SET_VALUE, payload: val });

// reducers.js

import { INCREMENT, DECREMENT, SET_VALUE } from './constants';
const initialState = { value: 0 };

export default function counterReducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return { ...state, value: state.value + 1 };
    case DECREMENT:
      return { ...state, value: state.value - 1 };
    case SET_VALUE:
      return { ...state, value: action.payload };
    default:
      return state;
  }
}
  1. 配置也很繁琐,中间件容易配置错误。
  • 创建 Store 极其麻烦,需要手动组合  combineRed  ucers 。
  • 想要异步?得手动安装并配置  redux-thun  k 。
  • 想要调试?得写那段又长又丑的  window.__R  EDUX_DEVTOOLS...  代码。
// ❌ 复杂的 Store 配置

const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer,
  comments: commentsReducer
});

const store = createStore(
  rootReducer,
  preloadedState,
  compose(
    applyMiddleware(thunk, logger),
    window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  )
);

Redux-Toolkit的优点

  1. 消灭样板代码过多(createSlice), 通过提供createSliceactiontypeaction creatorreducer三者的逻辑内聚到一个slice中。不用手写actiontypeaction creator,编写完slice之后,即可导出使用。
// features/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter', // 自动作为 action type 的前缀
  initialState: { value: 0 },
  reducers: {
    // 自动生成 action type: 'counter/increment'
    increment: (state) => {
      state.value += 1; 
    },
    decrement: (state) => {
      state.value -= 1;
    },
    // 自动生成 action creator: setValue(payload)
    setValue: (state, action) => {
      state.value = action.payload;
    },
  },
});

// 自动导出 Actions
export const { increment, decrement, setValue } = counterSlice.actions;
// 导出 Reducer
export default counterSlice.reducer;
  1. 开箱即用(configureStore) ,不需要纠结中间件顺序,不需要担心 DevTools 没配好。同时内置了Immer.js,解决了深层嵌套对象,需要写很复杂的  …  结构的情况。内置Redux-thunk,天生具备处理异步编程逻辑的能力。
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counterSlice';
// ... import other reducers
const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer,
    // ... RTK 会自动帮你调用 combineReducers
  },
  // Thunk? 默认有。
  // DevTools? 默认开。
});

Rematch

它采用了类似 Vuex 的 Model(模型) 架构,将  state 、 reducers (处理同步)和  effects (处理异步)内聚在同一个对象中,自动生成 Action Creators,彻底抛弃了繁琐的 Action Types 和 Switch 语句。

核心优势在于: 它既保留了 Redux 的不可变数据流和强大的中间件生态(兼容 Redux DevTools),又提供了极简的 API 和开箱即用的异步处理能力。简单来说,它让 React开发者也能享受到 Vuex 般丝滑的开发体验。

@rematch/core使用案例

import { init } from '@rematch/core';
// 1. 定义 Model
const count = {
  state: 0, // 初始状态,不用写繁琐的 switch case
  reducers: {
    // 纯函数,处理同步修改
    increment(state, payload) {
      return state + payload;
    },
    reset() {
      return 0; // 重置状态
    }
  },
  effects: (dispatch) => ({
    // 异步逻辑放在这里
    // payload 是参数,rootState 是全局状态
    async incrementAsync(payload, rootState) {
      // 模拟异步请求,比如等待 1 秒
      await new Promise(resolve => setTimeout(resolve, 1000));
      // 异步完成后,调用上面的 reducer 修改数据
      // 注意:这里 Rematch 自动把 dispatch 挂载到了 dispatch.count 上
      dispatch.count.increment(payload);
    }
  })
};

// 2. 初始化 Store
const store = init({
  models: { count }
});

// --- 使用演示 ---
// 触发同步 Reducer
store.dispatch.count.increment(1); 
// State 变为: 1
// 触发异步 Effect
store.dispatch.count.incrementAsync(5);
// (等待1秒后) State 变为: 6

个人觉得@rematch/core的使用还是很丝滑的,但是目前已经不维护了,且周下载量只有几w,不再推荐使用。

问题

  1. 介绍下redux
  1. 介绍下redux中间件机制
  1. 介绍下常用的redux中间件
  1. RTK解决了哪些问题?

看完文章之后可以看看这几个问题,来确认了解情况。

❌
❌