启源
服务端渲染MVC
Web 开发的早期阶段,服务端渲染(SSR) 是绝对的主流模式,浏览器的角色仅是 “渲染页面的容器”,用户发起请求后,服务器需要组装出包含完整数据的 HTML 页面,再整体响应给客户端。
为化解服务器端处理 "请求 - 数据 - 视图" 的复杂度,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 中更推荐使用
useSelectorHook 或connect来订阅状态变化,实现响应式更新
- 可以通过
-
-
Store 的方法:
getState():获取当前状态dispatch(action):分发一个 action,触发状态更新subscribe(listener):注册一个监听器(无参函数),当dispatch分发一个 action 之后,会运行注册的监听器。我们可以通过监听器来监听数据的变化(比如可在其中打印日志记录)。该函数会返回一个函数,用于取消监听replaceReducer(nextReducer):替换掉当前的 reducer(高级用法,很少使用)
-
Redux 数据流:
为了更好地理解 Redux,将Redux 与 MVC 架构模式的类比:
- Store(数据仓库) 相当于 Model(模型) :两者都负责存储和管理应用的数据状态
- Action + Reducer(动作 + 归约器) 相当于 Controller(控制器) :两者都负责处理业务逻辑和状态变更
- View(视图) 仍然是 View(视图) :负责用户界面的展示
示例:假设有一个账号密码组件,以登录功能为例
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' }) - 此时
state为undefined,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 的设计模式和架构分析
订阅机制
store.subscribe() 是一个全局订阅,无论哪个 reducer 更新,监听器都会被调用。在组件中由开发者自己提取需要的部分。
思想
1. 单向数据流架构
- 数据流向单一、可预测
- 每个状态变化都有明确的路径
- 便于调试和追踪
2. 不可变状态管理
设计模式协作关系
**实现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
- INIT 测试:确保 reducer 在初始化时(state 为 undefined)能返回有效状态
- UNKNOWN 测试:确保 reducer 在处理未知 action 时不会返回 undefined
为什么要测试 INIT?
当 createStore 创建 store 时,会调用一次 reducer 来初始化状态 如果 reducer 在 state 为 undefined 时返回 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 ❌ 验证失败,抛出错误
验证会在以下情况发现错误:
- 没有默认参数:
function reducer(state, action)而不是function reducer(state = {}, action) - 没有 default 分支:switch 语句或 if 语句没有处理所有情况
- 返回 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)之后继续执行(函数调用栈回退) - 这就是"洋葱模型":先一层层进入,再一层层返回 
第三方中间件
异步操作处理 - Redux-Thunk
Redux-Thunk 允许编写返回函数而不是动作对象的 action creators,实现异步操作。
注意:如果使用 Redux Toolkit,
redux-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:useSelector、useDispatch、useStore)。 |
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 的作用:将Reducer、Action Creator 和 Action 类型组合在一起,形成一个完整的状态管理单元
Slice 包含三个部分:
- name:Slice 的名称,用于生成 Action 类型的前缀(如
counter/increment) - initialState:该 Slice 的初始状态
- 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 函数会返回一个对象,包含以下属性:
-
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 -
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对象自动生成的。
工作原理:
-
你定义了
reducers对象中的函数(如increment、decrement) -
createSlice自动为每个 reducer 函数:- 生成对应的 Action 类型(如
'counter/increment') - 生成对应的 Action Creator(如
increment()) - 将这些 Action Creator 放入返回对象的
actions属性中
- 生成对应的 Action 类型(如
-
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 的 createSlice 和 createReducer 中编写 "mutation" 逻辑,因为它们在内部使用 Immer。如果在没有 Immer 的 reducer 中编写 mutation 逻辑,将改变状态并导致错误。
React-Redux
React-Redux:连接 React 和 Redux 的官方绑定库
- 提供
Provider组件:将 Redux store 注入到 React 应用 - 提供
connect高阶组件:连接 React 组件和 Redux store(传统方式) - 提供 Hooks API:
useSelector、useDispatch等(现代推荐方式) - 注意:
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 应用中使用,而
connect是 React-Redux 提供的高阶组件,专门用于连接 React 组件 和 Redux store。
connect 的作用:
- 将 Redux store 的 state 映射到 React 组件的 props
- 将 dispatch 方法和 action creators 映射到 React 组件的 props
- 自动订阅 store 的变化,当 state 更新时重新渲染组件
- 优化性能,只在相关 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(useSelector 和 useDispatch),可以完全替代 connect 和 bindActionCreators,使我们不需要手动使用 bindActionCreators,直接使用 dispatch 即可。
对比:
| 方式 | 获取状态 | 分发 Action | 特点 |
|---|---|---|---|
| connect | 通过 mapStateToProps | 通过 mapDispatchToProps 或 bindActionCreators | 高阶组件模式,需要包装组件 |
| 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替代了mapStateToProps和connect的状态映射功能useDispatch替代了mapDispatchToProps和bindActionCreators的功能- 不需要高阶组件包装,代码更简洁直观
性能优化提示:
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) // ✅ 使用浅比较