一文梳理Redux及衍生状态管理库 [共4000字-阅读时长20min]
Redux的基本组成
- state,所有的状态都以对象树的方式存在单一的store中。
- action,唯一修改state就是创建action,通过dispatch将action传递给reducer进行修改state。
- 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架构?
-
职责分离,action描述更新的内容,reducer描述怎么更新,state则描述管理状态。
-
构建出单向数据流,这种单向数据流让状态变化变得可预测、可调试。
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_state和dispatch。并且需要手动进行注册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订阅Redux的state变化,并在回调函数中出发组件的setState,实现UI的更新。这就React-Redux框架的作用。
什么是React-Redux?
React-redux是React和Redux的绑定库,它充当了数据层和视图层的桥梁,通过订阅Store的数据变化,自动触发组件的re-render更新视图,让Store的数据变化和useState可以达到一致的响应式方式。
React-Redux的组成:
- Provider组件
在React项目中,可以通过 Provider 组件,将创建的store对象注入到整个组件树中,任何想要使用Redux Store的子组件都可以通过useSelector和connect来访问。
- 简约使用,不用一层一层的传递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>
)
- useSelector,useDispatch钩子函数
useSelector的作用是从Redux Store中订阅读取需要的状态数据,并且这些状态都是响应式的,状态变化的时候会触发组件rerender。useDispatch用来获取React-redux的dispatch函数,派发action修改Redux Store的状态。
-
响应式数据,避免了繁琐的订阅数据的代码,使用useSelector天然就是响应式的。
-
hooks和函数式组件适配程度高
- 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处理的时候是一样的。不会发生处理之后actions的type就会从ADD_TO_CART,变为cart/ADD_TO_CART。combineReducers本质是将reducers物理分离管理(代码分离,不写在一起),对应的state的逻辑分离管理。
3.根Reducers接收到action的时候,会遍历所有的子Reducers,将actions传递进去,获得新的state值。
4.将所有的新的state值进行汇总然后返回。
还记得吗?Redux修改state的方法就是通过reducers返回新的值,来更新Store数据的。
中间件
![]()
Redux中间件采用AOP思想,在action到reducer的链路中插入切面逻辑,扩展自己的逻辑。
中间件的核心:增强dispatch的能力,让action到达reducers 之前可以进行一些操作处理。
洋葱模型
这是理解中间件执行顺序最关键的模型。Koa 的中间件也是这个原理。
想象你有一个洋葱,最核心(最里面)的是真实的store.dispatch(也就是派发给 Reducer) 。每一层洋葱皮就是一个中间件。
当你发起一个action时:
- 进场(In-bound): 就像一根针穿过洋葱,你需要先穿透最外层的皮(中间件1),再穿透第二层(中间件2)……直到刺中核心(Reducer)。
- 核心处理: Reducer 接收 action,更新 state。
- 出场(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;
}
让我把这三个箭头拆解开,你就懂了:
- 最外层 store :这是为了让中间件能获取状态 ( getState ) 或再次派发 ( dispatch )。
- 中间层 next :这是灵魂!
- next 并不总是指真实的 store.dispatch。
- 它指的是链条中的下一个中间件。
- 如果是最后一个中间件,它的
next才是真的store.dispatch。
- 如果你在中间件里不调用next(action),请求就会在这里断掉,永远到不了Reducer。
- 最内层 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')
执行流程是这样的:
- 用户调用dispatch(action) 。
- 进入 M1:
- 打印 'M1 Start'
- 遇到 next(action) -> 暂停 M1,进入 M2(因为 M1 的 next 就是 M2)。
- 进入 M2:
- 打印 'M2 Start'
- 遇到 next(action) -> 暂停 M2,进入 Reducer(因为 M2 是最后一个,它的 next 是真 dispatch)。
- Reducer 执行,Store 数据更新。
- 回溯(洋葱出场) :
- 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有什么缺点?
- 过多的样板代码
修改一个变量,要在 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;
}
}
- 配置也很繁琐,中间件容易配置错误。
- 创建 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的优点
-
消灭样板代码过多(createSlice), 通过提供
createSlice将actiontype、action creator和reducer三者的逻辑内聚到一个slice中。不用手写actiontype、action 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;
- 开箱即用(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,不再推荐使用。
问题
- 介绍下redux
- 介绍下redux中间件机制
- 介绍下常用的redux中间件
- RTK解决了哪些问题?
看完文章之后可以看看这几个问题,来确认了解情况。