【前端状态管理技术解析:Redux 与 Vue 生态对比】
前端状态管理技术解析:Redux 与 Vue 生态对比
基于 React、Vue 生态的技术分析与实践指南
目录
- 第一章:Redux 生态系统
- 第二章:Vue 状态管理演进
- 第三章:React vs Vue 响应式机制对比
- 第四章:异步状态管理
- 第五章:性能优化与缓存机制
- 第六章:跨框架状态管理
- 第七章:架构设计与最佳实践
第一章:Redux 生态系统
1.1 Redux 的核心概念
Redux 是一个可预测的 JavaScript 状态容器,它通过三个核心要素管理应用状态:
// Redux 的数据流
┌─────────────────────────────────────────┐
│ Redux Store │
│ (整个应用的状态树) │
└─────────────────────────────────────────┘▲│┌──────────┴──────────┐│ │┌────┴────┐ ┌─────┴─────┐│ Action │ │ Reducer ││ (做什么)│ │ (怎么做) │└─────────┘ └───────────┘
核心原则:
- 单一数据源:整个应用的 state 存储在一个 store 中
- State 是只读的:只能通过 dispatch action 来修改
- 使用纯函数进行修改:Reducer 必须是纯函数
1.2 传统 Redux 的完整实现
1.2.1 定义 Action Types
// actionTypes.js
// 为什么需要?避免字符串拼写错误,提供类型检查
export const INCREMENT = 'counter/INCREMENT'
export const DECREMENT = 'counter/DECREMENT'
export const ADD_USER = 'user/ADD_USER'
1.2.2 创建 Action Creators
// actions.js
// Action Creator 封装 action 的创建逻辑,避免重复代码export function increment() {return {type: 'counter/INCREMENT'}
}export function incrementByAmount(amount) {return {type: 'counter/INCREMENT_BY',payload: amount}
}export function addUser(user) {return {type: 'user/ADD_USER',payload: user}
}// 使用时:
// dispatch(increment()) // 不需要手写 { type: '...' }
// dispatch(incrementByAmount(5))
1.2.3 创建 Reducers
// counterReducer.js
const initialState = {value: 0
}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/INCREMENT_BY':return { ...state, value: state.value + action.payload }default:return state // 不认识的 action,返回原 state}
}// userReducer.js
const initialState = {users: []
}function userReducer(state = initialState, action) {switch (action.type) {case 'user/ADD_USER':return {...state,users: [...state.users, action.payload] // 数组也要不可变更新}default:return state}
}
1.2.4 使用 combineReducers 组合模块
// rootReducer.js
import { combineReducers } from 'redux'
import counterReducer from './counterReducer'
import userReducer from './userReducer'// combineReducers 把多个 reducer 组合成一个根 reducer
// 每个 reducer 只管理 state 树的一部分const rootReducer = combineReducers({counter: counterReducer, // 管理 state.counteruser: userReducer // 管理 state.user
})// 组合后的 state 结构:
// {
// counter: { value: 0 },
// user: { users: [] }
// }export default rootReducer
combineReducers 的工作原理:
// 简化版实现
function combineReducers(reducers) {return function combination(state = {}, action) {const nextState = {}// 对每个 reducer 调用,传入对应的 state 切片for (let key in reducers) {nextState[key] = reducers[key](state[key], action)}return nextState}
}
1.2.5 配置 Store 和中间件
// store.js
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk' // 异步中间件
import rootReducer from './rootReducer'// 配置 Redux DevTools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose// 应用中间件
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)))export default store
1.2.6 在 React 中使用
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import App from './App'ReactDOM.render(<Provider store={store}><App /></Provider>,document.getElementById('root')
)// Counter.js
import { useSelector, useDispatch } from 'react-redux'
import { increment, incrementByAmount } from './actions'function Counter() {// 读取 stateconst count = useSelector((state) => state.counter.value)// 获取 dispatch 函数const dispatch = useDispatch()return (<div><p>Count: {count}</p><button onClick={() => dispatch(increment())}>+1</button><button onClick={() => dispatch(incrementByAmount(5))}>+5</button></div>)
}
1.3 Redux Toolkit (RTK) 的现代化方案
Redux Toolkit 是 Redux 官方推荐的标准方式,大幅简化了开发体验。
1.3.1 安装依赖
# 只需要安装两个包
npm install @reduxjs/toolkit react-redux# @reduxjs/toolkit 自动包含:
# - redux (核心库)
# - immer (处理不可变更新)
# - redux-thunk (异步中间件)
# - reselect (性能优化)
1.3.2 使用 createSlice 创建模块
// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'// createSlice 自动完成:
// 1. 创建 action types
// 2. 创建 action creators
// 3. 创建 reducerconst counterSlice = createSlice({name: 'counter', // 用于生成 action type 前缀initialState: {value: 0},reducers: {// 自动生成 action creator: counterSlice.actions.increment// 自动生成 action type: 'counter/increment'increment: (state) => {state.value += 1 // 看起来可变,实际 Immer 会处理},decrement: (state) => {state.value -= 1},incrementByAmount: (state, action) => {state.value += action.payload}}
})// 导出自动生成的 action creators
export const { increment, decrement, incrementByAmount } = counterSlice.actions// 导出 reducer
export default counterSlice.reducer
createSlice 的魔法原理:
// createSlice 内部做了什么(简化版)
const counterSlice = {// 自动生成的 action creatorsactions: {increment: () => ({ type: 'counter/increment' }),decrement: () => ({ type: 'counter/decrement' }),incrementByAmount: (amount) => ({type: 'counter/incrementByAmount',payload: amount})},// 生成的 reducer(使用 Immer 处理不可变更新)reducer: function (state = initialState, action) {switch (action.type) {case 'counter/increment':return produce(state, (draft) => {draft.value += 1})// ...}}
}
1.3.3 使用 configureStore 配置
// store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter/counterSlice'
import userReducer from './features/user/userSlice'// configureStore 自动:
// 1. 调用 combineReducers
// 2. 添加 Redux DevTools
// 3. 添加 thunk 中间件
// 4. 添加开发环境检查const store = configureStore({reducer: {counter: counterReducer, // 自动 combineReducersuser: userReducer}
})export default store
1.3.4 代码量对比
传统 Redux:约 60 行代码
// actionTypes.js (5 行)
// actions.js (15 行)
// reducer.js (20 行)
// store.js (10 行)
// 总计:~50-60 行
Redux Toolkit:约 20 行代码
// counterSlice.js (15 行)
// store.js (5 行)
// 总计:~20 行,减少了 67%!
1.4 Redux 生态系统的组成
// 库的依赖关系
@reduxjs/toolkit├── redux (核心库)├── immer (不可变更新)├── redux-thunk (异步中间件)└── reselect (性能优化)react-redux (React 绑定)├── Provider (提供 store)├── useSelector (读取 state)├── useDispatch (获取 dispatch)└── connect (HOC,老式用法)
为什么不把 useSelector 放在 RTK 里?
- 设计哲学:关注点分离
- RTK 专注状态管理逻辑,与 UI 框架无关
- react-redux 专注 React 集成
- 可以在任何框架中使用 RTK(React、Vue、Angular)
历史原因:
2015: Redux 诞生 (状态管理核心)
2015: react-redux 诞生 (React 绑定)
2019: RTK 诞生 (Redux 的改进版)
1.5 Redux 的不可变数据原则
1.5.1 为什么需要不可变数据?
// 可预测性
reducer({ count: 0 }, { type: 'INCREMENT' }) // { count: 1 }
reducer({ count: 0 }, { type: 'INCREMENT' }) // { count: 1 }
// 永远相同!// 可追踪性
const history = [{ state: state0, action: action0 },{ state: state1, action: action1 }
]
// 可以随时重放,因为是纯函数// 性能优化
// React 可以通过引用比较快速判断是否需要重新渲染
oldState === newState // false,需要更新
1.5.2 传统 Redux 的手动不可变更新
// ❌ 错误:直接修改
function reducer(state, action) {state.count++ // 修改了原对象return state // 返回同一引用,React 不会检测到变化
}// ✅ 正确:创建新对象
function reducer(state, action) {return { ...state, count: state.count + 1 } // 新对象
}// 深层嵌套的更新很麻烦
function reducer(state, action) {return {...state,user: {...state.user,profile: {...state.user.profile,name: action.payload}}}
}
1.5.3 RTK 的 Immer 集成
// RTK 内部使用 Immer,可以写"可变"风格的代码
const slice = createSlice({reducers: {updateUser: (state, action) => {// 看起来是直接修改state.user.profile.name = action.payload// Immer 实际做的事:// return produce(state, draft => {// draft.user.profile.name = action.payload// })}}
})// 关键点:
// - 写起来像可变,实际是不可变
// - Immer 自动创建新对象
// - 引用仍然会变化:oldState !== newState
Immer 的工作原理:
import produce from 'immer'const currentState = {user: { name: 'John', age: 25 }
}const nextState = produce(currentState, (draft) => {draft.user.age = 26 // 修改 draft(代理对象)
})console.log(currentState.user.age) // 25 (原对象不变)
console.log(nextState.user.age) // 26 (新对象)
console.log(currentState !== nextState) // true
第二章:Vue 状态管理演进
2.1 Vuex 的核心概念
Vuex 是 Vue 的官方状态管理库(Vue 2/3 时代),与 Redux 有不同的设计哲学。
┌─────────────────────────────────────────┐
│ Vuex Store │
├─────────────────────────────────────────┤
│ State (状态) │
│ ↓ │
│ Getters (计算属性,可选) │
│ ↓ │
│ Mutations (同步修改,必须) │
│ ↓ │
│ Actions (异步操作,可选) │
└─────────────────────────────────────────┘
2.2 Vuex 的完整示例
// store.js (Vuex 3/4)
import { createStore } from 'vuex'const store = createStore({// 1️⃣ State - 存储数据state: {count: 0,users: []},// 2️⃣ Getters - 计算属性(类似 Vue 的 computed)getters: {doubleCount: (state) => state.count * 2,userCount: (state) => state.users.length},// 3️⃣ Mutations - 同步修改 state(必须是同步的!)// 为什么需要 Mutations?// - Vuex 规定:只能通过 mutation 修改 state// - 让状态变化可追踪(DevTools 可以记录每个 mutation)mutations: {INCREMENT(state) {state.count++ // 直接修改!Vuex 允许可变更新},INCREMENT_BY(state, payload) {state.count += payload},ADD_USER(state, user) {state.users.push(user) // 直接 push}},// 4️⃣ Actions - 异步操作,提交 mutation// 为什么需要 Actions?// - Mutations 必须是同步的,异步操作放在 Actionsactions: {async fetchUser({ commit }, userId) {const user = await api.getUser(userId)commit('ADD_USER', user) // 提交 mutation},incrementAsync({ commit }) {setTimeout(() => {commit('INCREMENT')}, 1000)}}
})
2.3 Mutation 的设计哲学
2.3.1 为什么必须通过 Mutation?
// ❌ Vuex 不允许直接修改 state
this.$store.state.count++ // 违反规则!虽然能工作,但不推荐// ✅ 必须通过 mutation
this.$store.commit('INCREMENT') // 正确方式
核心原因:
- 可追踪性:DevTools 可以记录每个 mutation
- 时间旅行调试:可以回放状态变化
- 明确的数据流:清楚知道谁修改了状态
2.3.2 Mutation 的严格规则
mutations: {// ✅ 同步操作INCREMENT(state) {state.count++},// ❌ 不能包含异步操作!ASYNC_INCREMENT(state) {setTimeout(() => {state.count++ // 违反规则!DevTools 无法追踪}, 1000)}
}actions: {// ✅ 异步操作放在 actionasyncIncrement({ commit }) {setTimeout(() => {commit('INCREMENT') // 提交同步 mutation}, 1000)}
}
2.4 Vuex 模块化
2.4.1 文件结构模块化
// 推荐的文件结构
store/
├── index.js # 组装模块并导出 store
├── modules/
│ ├── counter.js # counter 模块
│ ├── user.js # user 模块
│ └── todos.js # todos 模块
└── getters.js # 根级别的 getters(可选)
2.4.2 命名空间模块
// modules/counter.js
export default {namespaced: true, // 启用命名空间state: {count: 0},mutations: {increment(state) {state.count++}},actions: {incrementAsync({ commit }) {setTimeout(() => {commit('increment')}, 1000)}},getters: {doubleCount: state => state.count * 2}
}// store/index.js
import { createStore } from 'vuex'
import counter from './modules/counter'
import user from './modules/user'export default createStore({modules: {counter, // state.counteruser // state.user}
})// 使用时需要模块路径
store.commit('counter/increment')
store.dispatch('counter/incrementAsync')
store.getters['counter/doubleCount']
2.4.3 命名空间的优劣
特性 | 无命名空间 | 有命名空间 |
---|---|---|
调用方式 | commit('increment') | commit('counter/increment') |
命名冲突 | ❌ 容易冲突 | ✅ 不会冲突 |
代码组织 | ⚠️ 混在一起 | ✅ 清晰分离 |
适用场景 | 小项目 | 中大型项目 |
2.5 Pinia:Vuex 的继任者(Vuex 5)
Pinia 是 Vue 3 官方推荐的状态管理库,完全取消了 Mutation!
2.5.1 Pinia 的简化设计
// store.js (Pinia)
import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', {// Statestate: () => ({count: 0,users: []}),// Getters (同 Vuex)getters: {doubleCount: (state) => state.count * 2},// Actions - 同时处理同步和异步!// 不再区分 mutations 和 actionsactions: {// 同步操作increment() {this.count++ // 直接修改 state!},incrementBy(amount) {this.count += amount},// 异步操作async fetchUser(userId) {const user = await api.getUser(userId)this.users.push(user) // 直接修改!}}
})
2.5.2 Pinia 为什么取消 Mutation?
- 简化 API:Mutation 和 Action 的区分让新手困惑
- TypeScript 支持更好:减少样板代码
- 现代 DevTools:新的调试工具可以追踪 action 内的所有变化
- 对齐其他库:与 Redux Toolkit 等保持一致
// Vuex 3/4 - 需要区分
mutations: { INCREMENT(state) { state.count++ } }
actions: { async fetch() { /* ... */ } }// Pinia - 统一到 actions
actions: {increment() { this.count++ }, // 同步async fetch() { /* ... */ } // 异步
}
2.5.3 Pinia 的使用
// Counter.vue
<template><div><p>Count: {{ counter.count }}</p><p>Double: {{ counter.doubleCount }}</p><button @click="counter.increment()">+1</button><button @click="counter.incrementBy(5)">+5</button></div>
</template><script setup>
import { useCounterStore } from './store'const counter = useCounterStore()// 也可以直接修改 state(不推荐,但允许)
// counter.count++// 推荐使用 actions
counter.increment()
</script>
第三章:React vs Vue 响应式机制对比
3.1 核心设计哲学差异
3.1.1 React 的手动追踪
// React: 显式更新,手动告诉框架"我要更新了"
function Counter() {const [count, setCount] = useState(0)// ❌ 不会触发重渲染count = count + 1// ✅ 必须调用 settersetCount(count + 1)
}// 设计哲学:
// UI = f(state) // 纯函数
// 新 state → 返回新 UI
3.1.2 Vue 的自动追踪
// Vue: 隐式更新,自动知道"你更新了"
const count = ref(0)
count.value++ // Vue 自动检测到变化并重新渲染// 设计哲学:
// 响应式代理 → 自动追踪依赖 → 自动更新
3.2 响应式系统对比
3.2.1 React 的不可变更新
// React 必须创建新对象
const [user, setUser] = useState({ name: 'John', age: 25 })// 更新必须创建新引用
setUser({ ...user, age: 26 })// 深层嵌套更麻烦
setUser({...user,profile: {...user.profile,address: {...user.profile.address,city: 'New York'}}
})
3.2.2 Vue 的可变更新
// Vue 2 - Object.defineProperty
const vm = new Vue({data: {count: 0,user: { name: 'John' }}
})vm.count = 1 // ✅ 可以检测到
vm.user.age = 25 // ❌ 新属性检测不到(需要 Vue.set)// Vue 3 - Proxy
const state = reactive({count: 0,user: { name: 'John' }
})state.count = 1 // ✅ 可以检测到
state.user.age = 25 // ✅ 可以检测到(深层响应式)
state.newArray = [1, 2, 3] // ✅ 新属性也能检测到
3.3 组件更新机制
3.3.1 React 的自顶向下更新
// React - 父组件更新,子组件也重新执行
function Parent() {const [count, setCount] = useState(0)return (<div><Child /> {/* Parent 更新,Child 也重新执行 */}</div>)
}// 需要手动优化
const MemoChild = React.memo(Child) // 手动优化
3.3.2 Vue 的精确更新
<!-- Vue - 精确的依赖追踪 -->
<template><div><Child /><!-- Parent 更新,Child 不受影响(除非 props 变化) --></div>
</template><!-- Vue 自动优化,不需要手动 memo -->
3.4 完整对比表
特性 | React | Vue |
---|---|---|
响应式 | 手动(setState) | 自动(Proxy) |
更新粒度 | 组件级别 | 属性级别 |
性能优化 | 手动(memo, useMemo) | 自动 |
模板 | JSX(运行时) | Template(编译时) |
学习曲线 | 陡峭(JS 为主) | 平缓(HTML 为主) |
灵活性 | 高(纯 JS) | 中(指令+JS) |
TypeScript | 优秀 | 优秀(Vue 3) |
生态 | 巨大 | 大 |
3.5 高阶函数的使用
3.5.1 React 的函数式风格
// 类组件时代 - 高阶组件(HOC)
const EnhancedComponent = withRouter(withAuth(MyComponent))// Hook 时代 - 高阶函数
const [count, setCount] = useState(0) // 返回函数
useEffect(() => {return () => {} // 返回清理函数
}, [])// Redux - 高阶函数
const dispatch = useDispatch()
const selector = useSelector((state) => state.count) // 函数参数
3.5.2 Vue 的声明式风格
// Vue 2 - 选项式 API
data() {return { count: 0 }
}
this.count++ // 直接修改// Vue 3 - 组合式 API(更函数式)
const count = ref(0)
count.value++ // 仍然是赋值
第四章:异步状态管理
4.1 为什么需要异步处理?
4.1.1 核心问题:Reducer 必须是纯函数
// ❌ 如果允许在 reducer 中直接异步
function userReducer(state, action) {switch (action.type) {case 'FETCH_USER':// 问题来了!fetch('/api/user').then((data) => {// 这时候该怎么更新 state?// state 已经返回了!return { ...state, user: data } // 太晚了!})return state // 必须立即返回,但数据还没获取到!}
}
Reducer 必须是纯函数的原因:
- 可预测性:相同输入必定产生相同输出
- 可测试性:易于单元测试
- 可追踪性:DevTools 可以记录每个状态变化
- 时间旅行:可以回放历史状态
4.1.2 完整的异步操作需要三个状态
// 一个异步请求有三种可能的结果:
{loading: true, // 1. 加载中data: null,error: null
}{loading: false, // 2. 成功data: { ... },error: null
}{loading: false, // 3. 失败data: null,error: 'Network error'
}
4.2 Redux 的异步解决方案
4.2.1 Redux 的数据流
// Redux 的中间件机制
dispatch(action) → [中间件] → reducer → new state↑异步操作在这里!
4.2.2 Redux Thunk 的原理
// Thunk 是什么?
// Thunk = "延迟执行的函数"// 普通值 - 立即计算
const value = 1 + 2 // 立即得到 3// Thunk - 延迟计算
const thunk = () => 1 + 2 // 返回函数,不立即执行
const value = thunk() // 调用时才计算// 在 Redux 中:
// 普通 action - 立即执行
dispatch({ type: 'INCREMENT' })// Thunk action - 延迟执行
dispatch(() => {// dispatch 一个函数!setTimeout(() => {dispatch({ type: 'INCREMENT' })}, 1000)
})
Thunk 中间件的实现(非常简单!):
// 完整的 redux-thunk 源码
function createThunkMiddleware(extraArgument) {return ({ dispatch, getState }) =>(next) =>(action) => {// 如果 action 是函数,执行它if (typeof action === 'function') {return action(dispatch, getState, extraArgument)}// 否则传递给下一个中间件return next(action)}
}const thunk = createThunkMiddleware()
4.2.3 手写 Thunk Action
// actions.js
export const fetchUser = (userId) => {// 返回一个函数,而不是 action 对象!return async (dispatch, getState) => {// 1. 开始加载dispatch({ type: 'FETCH_USER_REQUEST' })try {// 2. 异步操作const response = await fetch(`/api/users/${userId}`)const data = await response.json()// 3. 成功dispatch({type: 'FETCH_USER_SUCCESS',payload: data})} catch (error) {// 4. 失败dispatch({type: 'FETCH_USER_FAILURE',payload: error.message})}}
}// userReducer.js
function userReducer(state = initialState, action) {switch (action.type) {case 'FETCH_USER_REQUEST':return { ...state, loading: true, error: null }case 'FETCH_USER_SUCCESS':return {...state,loading: false,data: action.payload}case 'FETCH_USER_FAILURE':return {...state,loading: false,error: action.payload}default:return state}
}// 组件中使用
function UserProfile() {const dispatch = useDispatch()useEffect(() => {dispatch(fetchUser(123))}, [])
}
4.3 Redux Toolkit 的 createAsyncThunk
4.3.1 为什么需要 extraReducers?
理解 reducers vs extraReducers:
const userSlice = createSlice({name: 'user',initialState: { data: null, loading: false },// reducers: 处理内部 actions// 自动生成 action creators: user/setNamereducers: {setName: (state, action) => {state.data.name = action.payload}// 这里只能处理 "user/xxx" 的 actions},// extraReducers: 处理外部 actions// 处理来自 createAsyncThunk 的 actionsextraReducers: (builder) => {// 处理 "user/fetch/pending", "user/fetch/fulfilled" 等builder.addCase(fetchUser.fulfilled, (state, action) => {state.data = action.payload})}
})
概念关系图:
createAsyncThunk → 自动生成 3 个 action types↓user/fetch/pendinguser/fetch/fulfilleduser/fetch/rejected↓
extraReducers → 处理这些外部 actions
4.3.2 完整的 AsyncThunk 实现
// 第 1 步:创建 AsyncThunk
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'const fetchUser = createAsyncThunk('user/fetchUser', // action type 前缀async (userId, thunkAPI) => {// thunkAPI 包含很多工具const { dispatch, getState, rejectWithValue, signal } = thunkAPI// 可以访问当前 stateconst currentState = getState()if (currentState.user.data) {return currentState.user.data // 已有数据,不重复请求}try {const response = await fetch(`/api/users/${userId}`, { signal })const data = await response.json()if (!response.ok) {// 自定义错误return rejectWithValue(data.error)}return data // 成功时返回的数据} catch (error) {return rejectWithValue(error.message)}}
)// RTK 自动生成:
// fetchUser.pending → 'user/fetchUser/pending'
// fetchUser.fulfilled → 'user/fetchUser/fulfilled'
// fetchUser.rejected → 'user/fetchUser/rejected'// 第 2 步:在 Slice 中处理
const userSlice = createSlice({name: 'user',initialState: {data: null,loading: false,error: null},reducers: {// 同步 actionsclearUser: (state) => {state.data = null}},extraReducers: (builder) => {// builder.addCase = "当收到这个 action 时,执行这个函数"builder.addCase(fetchUser.pending, (state) => {state.loading = truestate.error = null}).addCase(fetchUser.fulfilled, (state, action) => {state.loading = falsestate.data = action.payload // payload 是 async 函数的返回值}).addCase(fetchUser.rejected, (state, action) => {state.loading = falsestate.error = action.payload || action.error.message})}
})export const { clearUser } = userSlice.actions
export default userSlice.reducer// 第 3 步:组件中使用
function UserProfile() {const dispatch = useDispatch()const { data, loading, error } = useSelector((state) => state.user)useEffect(() => {// dispatch 返回一个 Promisedispatch(fetchUser(123)).unwrap() // 提取 payload.then((data) => console.log('Success:', data)).catch((error) => console.error('Failed:', error))}, [])if (loading) return <p>Loading...</p>if (error) return <p>Error: {error}</p>if (data) return <div>{data.name}</div>return null
}
4.3.3 执行时序详解
// 问题:useEffect 和 loading 判断是同步执行的吗?function UserProfile() {const { loading } = useSelector((state) => state.user) // 初始: falseconst dispatch = useDispatch()useEffect(() => {dispatch(fetchUser(123)) // 立即发起请求}, [])// 渲染逻辑if (loading) return <p>Loading...</p> // 第 1 次渲染不会显示return <div>Content</div> // 第 1 次显示这个
}// 详细时序:
/*
1. 组件首次渲染- loading = false (初始值)- useEffect 执行: dispatch(fetchUser(123))- 显示: <div>Content</div>2. dispatch 执行- Redux 收到 'user/fetchUser/pending'- reducer 执行: state.loading = true- state 变化!3. state 变化触发重新渲染- loading = true (新值)- 显示: <p>Loading...</p>4. 网络请求完成- Redux 收到 'user/fetchUser/fulfilled'- reducer 执行: state.loading = false, state.data = ...- state 再次变化!5. 再次重新渲染- loading = false, data = {...}- 显示: <div>{data.name}</div>
*/
4.4 Vuex 和 Pinia 的异步处理
4.4.1 Vuex 的 Action
// Vuex 的数据流
dispatch(action) → action 函数 → commit(mutation) → state 变化const store = createStore({state: {user: null,loading: false,error: null},mutations: {// Mutations 必须同步!SET_LOADING(state, loading) {state.loading = loading},SET_USER(state, user) {state.user = user},SET_ERROR(state, error) {state.error = error}},actions: {// Action 可以异步!async fetchUser({ commit }, userId) {commit('SET_LOADING', true)try {const response = await fetch(`/api/users/${userId}`)const data = await response.json()commit('SET_USER', data)} catch (error) {commit('SET_ERROR', error.message)} finally {commit('SET_LOADING', false)}}}
})// 使用
store.dispatch('fetchUser', 123)
4.4.2 Pinia 的统一 Action
// Pinia 简化了!不需要 mutation
export const useUserStore = defineStore('user', {state: () => ({user: null,loading: false,error: null}),actions: {// Action 可以直接修改 state,可以异步!async fetchUser(userId) {this.loading = truethis.error = nulltry {const response = await fetch(`/api/users/${userId}`)this.user = await response.json() // 直接修改!} catch (error) {this.error = error.message} finally {this.loading = false}}}
})// 使用
const userStore = useUserStore()
userStore.fetchUser(123)
4.5 为什么 Pinia 可以统一同步/异步?
4.5.1 核心区别:设计哲学
Redux 的限制:
Reducer 必须立即返回 → 不能异步 → 需要 ThunkPinia 的自由:
Action 不需要立即返回 → 可以异步 → Vue 响应式自动追踪每次修改
4.5.2 技术原因对比
Redux 为什么不能异步?
// Redux reducer 的要求
function reducer(state, action) {// 必须立即返回新 statereturn newState // 不能是 Promise!
}// 如果允许异步:
function reducer(state, action) {fetch('/api').then((data) => {return { ...state, data } // ❌ 太晚了!})return state // 必须现在就返回
}
Pinia 为什么可以异步?
// Pinia action 可以是任何函数
actions: {async fetchData() {this.loading = true // 修改 1: 立即生效,响应式追踪const data = await fetch() // 等待...this.data = data // 修改 2: 稍后生效,响应式追踪this.loading = false // 修改 3: 再稍后生效,响应式追踪}
}// Vue 的响应式系统自动追踪每次修改!
// DevTools 可以记录所有变化
4.5.3 可预测性的实现方式
Redux: 纯函数 Reducer → 相同输入必定相同输出 → 可预测Pinia: 响应式追踪 → 记录所有变化 → 可重放 → 可预测
4.6 异步状态管理最佳实践
4.6.1 统一的异步状态结构
// 推荐的异步状态结构
const asyncState = {data: null, // 数据loading: false, // 加载状态error: null, // 错误信息timestamp: null, // 最后更新时间hasLoaded: false // 是否曾经加载过
}// 使用示例
const userSlice = createSlice({name: 'user',initialState: {profile: { data: null, loading: false, error: null },posts: { data: [], loading: false, error: null }}// ...
})
4.6.2 取消请求
// RTK 的取消机制
const fetchUser = createAsyncThunk('user/fetch', async (userId, { signal }) => {const response = await fetch(`/api/users/${userId}`, {signal // 传递 AbortSignal})return response.json()
})// 组件中
function UserProfile() {const dispatch = useDispatch()useEffect(() => {const promise = dispatch(fetchUser(123))return () => {promise.abort() // 组件卸载时取消请求}}, [])
}
4.6.3 请求去重
// 避免重复请求
const fetchUser = createAsyncThunk('user/fetch', async (userId, { getState, rejectWithValue }) => {const state = getState()// 如果正在加载,拒绝新请求if (state.user.loading) {return rejectWithValue('Already loading')}// 如果已有数据且时间未过期,使用缓存const cacheTime = 5 * 60 * 1000 // 5 分钟if (state.user.data && Date.now() - state.user.timestamp < cacheTime) {return state.user.data}const response = await fetch(`/api/users/${userId}`)return response.json()
})
第五章:性能优化与缓存机制
5.1 useSelector 的工作原理
5.1.1 useSelector 的来源
// useSelector 来自 react-redux,不是 RTK!
import { useSelector } from 'react-redux' // ← 来自 react-redux
import { createSlice } from '@reduxjs/toolkit' // ← 来自 RTK// 库的关系:
// RTK → 状态管理逻辑
// react-redux → React 和 Redux 的连接器
// React → UI 框架
5.1.2 useSelector 必须在 Provider 内使用
// Provider 使用 React Context 提供 store
import { Provider } from 'react-redux'// 必须的设置
function App() {return (<Provider store={store}>{' '}{/* 必须有这个! */}<Counter /></Provider>)
}// useSelector 内部的简化实现
function useSelector(selector) {const store = useContext(ReduxContext) // 从 Context 获取 storeif (!store) {throw new Error('useSelector must be used within a Provider')}const [, forceRender] = useReducer((s) => s + 1, 0)const lastResultRef = useRef()// 计算新结果const currentResult = selector(store.getState())// 默认用 === 比较if (currentResult !== lastResultRef.current) {lastResultRef.current = currentResult// 订阅 store 变化store.subscribe(() => {const newResult = selector(store.getState())if (newResult !== lastResultRef.current) {forceRender() // 触发重渲染}})}return currentResult
}
5.1.3 useSelector 的比较机制
关键:useSelector 默认使用严格相等比较(===)
// 问题场景:返回新对象
function Counter() {const data = useSelector((state) => ({count: state.counter.value,name: state.user.name}))// 问题分析:// 第 1 次渲染: { count: 0, name: 'John' } // 对象 A// 第 2 次渲染: { count: 0, name: 'John' } // 对象 B// 对象 A !== 对象 B // 引用不同!// → 触发重新渲染// → 又创建新对象// → 无限循环!console.log('渲染了') // 会疯狂打印!
}
为什么每次都是新对象?
// 每次调用函数都创建新对象
function makeObject() {return { a: 1 }
}const obj1 = makeObject() // 对象 A
const obj2 = makeObject() // 对象 B
obj1 !== obj2 // true,不同引用!// JavaScript 对象比较的本质
{} === {} // false!
{ a: 1 } === { a: 1 } // false!const obj1 = { a: 1 }
const obj2 = obj1
obj1 === obj2 // true (同一个引用)
5.2 解决 useSelector 的性能问题
5.2.1 方案 1:分开选择(推荐)
// ✅ 每个值单独选择
function Counter() {const count = useSelector((state) => state.counter.value) // 数字const name = useSelector((state) => state.user.name) // 字符串// 原始值的比较:// 0 === 0 ✅ 相等,不重渲染// 'John' === 'John' ✅ 相等,不重渲染return (<div>{count} - {name}</div>)
}
5.2.2 方案 2:使用 shallowEqual
// ✅ 浅层比较对象的每个属性
import { useSelector, shallowEqual } from 'react-redux'function Counter() {const { count, name } = useSelector((state) => ({count: state.counter.value,name: state.user.name}),shallowEqual // 第二个参数:自定义比较函数)return (<div>{count} - {name}</div>)
}// shallowEqual 的实现
function shallowEqual(objA, objB) {// 1. 引用相同if (objA === objB) return true// 2. 比较属性数量const keysA = Object.keys(objA)const keysB = Object.keys(objB)if (keysA.length !== keysB.length) return false// 3. 浅层比较每个属性的值for (let key of keysA) {if (objA[key] !== objB[key]) return false // 浅比较}return true
}// 工作原理:
// 第 1 次: { count: 0, name: 'John' }
// 第 2 次: { count: 0, name: 'John' }
// shallowEqual 比较:
// oldObj.count === newObj.count // 0 === 0 ✅
// oldObj.name === newObj.name // 'John' === 'John' ✅
// → 认为相等,不重渲染!
5.2.3 方案 3:Reselect 缓存
// ✅ 使用 createSelector 缓存结果
import { createSelector } from '@reduxjs/toolkit'const selectCounterData = createSelector(// 输入 selectors[(state) => state.counter.value, (state) => state.user.name],// 输出函数(count, name) => ({ count, name })
)function Counter() {const data = useSelector(selectCounterData)// 只有当 count 或 name 变化时,才重新创建对象// 否则返回缓存的对象(同一个引用)return (<div>{data.count} - {data.name}</div>)
}
5.3 Reselect 详解
5.3.1 什么是 Selector?
Selector 是从 Redux state 中选择数据的函数。
// 这就是一个 selector 函数
const selectCount = (state) => state.counter.count
// ↑ 函数名 ↑ 从整个 state 中选择 count// 使用
const count = useSelector(selectCount)
5.3.2 什么是 Reselect?
Reselect 创建带缓存的 selector,避免重复计算。
// 问题:没有缓存的昂贵计算
function ExpensiveComponent() {const expensiveData = useSelector((state) => {console.log('计算中...') // 每次渲染都会执行!return state.items.filter((item) => item.active).sort((a, b) => a.name.localeCompare(b.name)).reduce((sum, item) => sum + item.price, 0)})return <div>{expensiveData}</div>
}// 解决:使用 Reselect 缓存
const selectExpensiveData = createSelector([(state) => state.items], // 输入(items) => {console.log('计算中...') // 只在 items 变化时执行return items.filter((item) => item.active).sort((a, b) => a.name.localeCompare(b.name)).reduce((sum, item) => sum + item.price, 0)}
)function ExpensiveComponent() {const expensiveData = useSelector(selectExpensiveData)return <div>{expensiveData}</div>
}
5.3.3 为什么叫 “Reselect”?
"Re-select" = "重新选择"普通 select: 每次都选择(重新计算)
Re-select: 智能重新选择(只在依赖变化时才重新计算)
5.3.4 Reselect 的工作原理
// 简化版的 createSelector 实现
function createSelector(inputSelectors, resultFunc) {let lastInputs = []let lastResultreturn (state) => {// 1. 获取当前输入const currentInputs = inputSelectors.map((selector) => selector(state))// 2. 比较输入是否变化(浅比较)const inputsChanged = currentInputs.some((input, index) => input !== lastInputs[index])// 3. 只有变化时才重新计算if (inputsChanged) {lastInputs = currentInputslastResult = resultFunc(...currentInputs)}// 4. 返回结果(可能是缓存的)return lastResult}
}
5.3.5 Reselect 的存储位置
缓存存储在函数闭包中,全局有效:
// 每个 selector 实例都有自己的闭包
const selectTotalPrice = createSelector(...) // 闭包 1
const selectTotalTax = createSelector(...) // 闭包 2function ComponentA() {const total = useSelector(selectTotalPrice) // 第一次计算,存储缓存return <div>{total}</div>
}function ComponentB() {const total = useSelector(selectTotalPrice) // 使用相同缓存!return <span>{total}</span>
}// 即使 ComponentA 卸载了,selectTotalPrice 的缓存依然存在
// 因为缓存存储在 selector 函数的闭包中,不在组件中
5.3.6 Reselect 的内存管理
默认只缓存最后一次的结果:
const selectExpensiveData = createSelector(...)// 第 1 次调用
selectExpensiveData(state1) // lastInputs = [...], lastResult = result1// 第 2 次调用:输入变化
selectExpensiveData(state2) // lastInputs = [...], lastResult = result2// result1 被覆盖,可以被垃圾回收!// 所以不会无限累积内存!
内存风险场景:
// ❌ 在组件中动态创建 selector
function UserComponent({ userId }) {// 每次渲染都创建新的 selector!const selectUser = createSelector([(state) => state.users], (users) =>users.find((u) => u.id === userId))// 如果这个组件被创建很多次,会有很多 selector 实例
}// ✅ 正确:在模块级别定义
const selectUsers = (state) => state.usersfunction UserComponent({ userId }) {// 使用 useMemo 管理动态 selectorconst selectUser = useMemo(() => createSelector([selectUsers], (users) => users.find((u) => u.id === userId)),[userId])const user = useSelector(selectUser)
}
5.4 Reselect vs Pinia Getter
5.4.1 核心相似点
都是计算属性 + 自动缓存:
// RTK Reselect
const selectTotalPrice = createSelector([(state) => state.cart.items], (items) => {console.log('计算总价') // 只在 items 变化时执行return items.reduce((total, item) => total + item.price, 0)
})const totalPrice = useSelector(selectTotalPrice)// ==========================================// Pinia Getter
const useCartStore = defineStore('cart', {state: () => ({ items: [] }),getters: {totalPrice: (state) => {console.log('计算总价') // 只在 items 变化时执行return state.items.reduce((total, item) => total + item.price, 0)}}
})const cart = useCartStore()
const totalPrice = cart.totalPrice
5.4.2 底层实现对比
特性 | RTK Reselect | Pinia Getter |
---|---|---|
缓存机制 | 手动实现 | Vue computed |
依赖追踪 | 手动声明 | 自动追踪 |
使用方式 | useSelector(selector) | store.getterName |
存储位置 | 函数闭包 | 组件实例 |
生命周期 | 永久存在 | 跟随 store |
5.4.3 缓存机制对比
// Reselect: 手动实现的缓存
function createSelector(inputSelectors, resultFunc) {let lastArgs = nulllet lastResult = nullreturn (state) => {const newArgs = inputSelectors.map((selector) => selector(state))// 手动比较参数是否变化if (lastArgs === null || argsChanged(newArgs, lastArgs)) {lastArgs = newArgslastResult = resultFunc(...newArgs)}return lastResult}
}// Pinia Getter: 基于 Vue 的 computed
getters: {doubleCount: (state) => state.count * 2// 等价于:// computed(() => state.count * 2)// Vue 自动追踪依赖,自动缓存
}
5.5 useSelector vs useMemo
5.5.1 核心区别
// useSelector: 从 Redux store 读取数据
const count = useSelector((state) => state.counter.value)
// 1. 订阅 Redux store
// 2. store 变化时自动重新计算
// 3. 结果变化时触发重渲染// useMemo: 缓存计算结果
const doubleCount = useMemo(() => count * 2, [count])
// 1. 不订阅任何东西
// 2. 只在依赖项 [count] 变化时重新计算
// 3. 用于优化性能
5.5.2 为什么不能用 useMemo 替代 useSelector?
// ❌ 不能用 useMemo 替代 useSelector
import { useContext } from 'react'function Counter() {const store = useContext(ReduxContext)// ❌ 这样写不会在 store 变化时重新渲染!const count = useMemo(() => store.getState().counter.value,[store] // store 引用从不变化!)return <div>{count}</div>
}// ✅ useSelector 内部订阅了 store
function Counter() {const count = useSelector((state) => state.counter.value)// useSelector 做的事:// 1. 从 context 获取 store// 2. store.subscribe(callback) // 订阅变化!// 3. 变化时重新计算并触发重渲染return <div>{count}</div>
}
5.5.3 组合使用
function ExpensiveComponent() {// 1. 用 useSelector 从 Redux 读取const items = useSelector(state => state.items)const filter = useSelector(state => state.filter)// 2. 用 useMemo 缓存昂贵计算const filteredItems = useMemo(() => {console.log('过滤中...')return items.filter(item => item.type === filter)}, [items, filter])// 3. 再用 useMemo 缓存二次计算const sortedItems = useMemo(() => {console.log('排序中...')return [...filteredItems].sort((a, b) =>a.name.localeCompare(b.name))}, [filteredItems])return <div>{sortedItems.map(...)}</div>
}
5.6 各种缓存机制的存储位置
5.6.1 React useMemo 的存储
// 存储位置: 组件实例的 fiber 节点// 简化的 React fiber 结构
const ComponentFiber = {type: MyComponent,memoizedState: [// hooks 链表{memoizedState: 'cached value', // ← useMemo 的缓存deps: [dep1, dep2], // 依赖数组next: nextHook}]
}// 生命周期:跟随组件
// 组件挂载 → 创建 fiber → 初始化 hook
// 组件卸载 → 销毁 fiber → 清理所有缓存
5.6.2 Vue computed 的存储
// 存储位置: 组件实例的 effects 对象const ComponentInstance = {effects: new Set(),scope: new EffectScope(),computedEffect: {value: 'cached result', // ← computed 的缓存值dirty: false, // 是否需要重新计算deps: new Set([state.count]) // 自动收集的依赖}
}// 生命周期:跟随组件
// 组件创建 → 创建 effect
// 组件销毁 → 清理 effect
5.6.3 RTK Reselect 的存储
// 存储位置: selector 函数的闭包function createSelector(inputSelectors, resultFunc) {let lastArgs = undefined // ← 缓存存储在闭包中let lastResult = undefined // ← 缓存存储在闭包中return function memoizedSelector(state) {// ...}
}// 生命周期:永久存在(除非 selector 被垃圾回收)
// 创建: const select = createSelector(...)
// 销毁: select = null (手动清理)
5.6.4 Pinia Getter 的存储
// 存储位置: store 实例的响应式对象function defineStore(id, options) {const state = reactive(options.state())const getters = {}Object.keys(options.getters).forEach((key) => {getters[key] = computed(() => {return options.getters[key].call(store, state)})})return { ...state, ...getters }
}// 生命周期:跟随 store
// store 创建 → 创建 computed
// store 销毁 → 清理 computed
5.6.5 缓存存储总结
技术 | 存储位置 | 生命周期 | 作用域 |
---|---|---|---|
React useMemo | 组件 fiber 节点 | 跟随组件 | 组件级别 |
Vue computed | 组件实例 effects | 跟随组件 | 组件级别 |
RTK Reselect | 函数闭包 | 永久存在 | 全局 |
Pinia Getter | Store computed | 跟随 store | 全局 |
第六章:跨框架状态管理
6.1 框架无关的状态管理库
6.1.1 库的分类
库 | 框架绑定 | 能否跨框架 | 通用性 |
---|---|---|---|
Redux Core | 框架无关(核心) | ✅ 可以 | 最通用 |
MobX | 框架无关 | ✅ 可以 | 很通用 |
Zustand | 理论上可以 | ⚠️ 主要 React | 一般 |
Jotai/Recoil | React 专用 | ❌ 不能 | React 专用 |
Vuex/Pinia | Vue 专用 | ❌ 不能 | Vue 专用 |
6.1.2 Redux Core 的跨框架使用
// Redux 核心是框架无关的
import { createStore } from 'redux'const store = createStore(reducer)// ==========================================
// 在 Vanilla JS 中使用
store.subscribe(() => {document.getElementById('count').textContent = store.getState().count
})document.getElementById('btn').onclick = () => {store.dispatch({ type: 'INCREMENT' })
}// ==========================================
// 在 React 中使用
import { Provider } from 'react-redux'
;<Provider store={store}>...</Provider>// ==========================================
// 在 Vue 中使用
import { createApp } from 'vue'
const app = createApp(App)
app.config.globalProperties.$store = store// ==========================================
// 在 Angular 中使用
import { Store } from '@ngrx/store' // Redux 的 Angular 版本
6.1.3 MobX 的跨框架能力
// store.js - MobX,框架无关
import { makeObservable, observable, action } from 'mobx'class CounterStore {count = 0constructor() {makeObservable(this, {count: observable,increment: action})}increment() {this.count++}
}export const counterStore = new CounterStore()// ==========================================
// React 中使用
import { observer } from 'mobx-react-lite'const Counter = observer(() => {return <button onClick={() => counterStore.increment()}>{counterStore.count}</button>
})// ==========================================
// Vue 中使用
import { observer } from 'mobx-vue'export default observer({render() {return <button onClick={() => counterStore.increment()}>{counterStore.count}</button>}
})
6.2 为什么 Pinia/Vuex 不能跨框架?
6.2.1 深度依赖 Vue 响应式系统
// Pinia 内部使用 Vue 的 reactive 和 computed
import { reactive, computed } from 'vue' // 必须依赖 Vue!export function defineStore(name, options) {const state = reactive(options.state()) // Vue 的 reactiveconst getters = computed(() => ...) // Vue 的 computed// 无法在 React 中用!
}
6.2.2 设计初衷
Redux Core:
- 设计目标:通用状态管理
- 核心:纯 JavaScript 对象 + 纯函数
- 可以在任何环境使用Pinia/Vuex:
- 设计目标:Vue 专用状态管理
- 核心:Vue 响应式系统
- 深度集成 Vue 生态
6.3 Zustand:轻量级的状态管理
6.3.1 Zustand 的特点
// Zustand - 极简的状态管理
import { create } from 'zustand'// 一步到位!不需要 Provider、slice、reducer
const useStore = create((set) => ({count: 0,increment: () => set((state) => ({ count: state.count + 1 }))
}))// 直接使用,不需要 Provider!
function Counter() {const { count, increment } = useStore()return <button onClick={increment}>{count}</button>
}
6.3.2 Zustand vs Redux 对比
特性 | Redux (RTK) | Zustand |
---|---|---|
样板代码 | 中等 | 极少 |
需要 Provider | ✅ 需要 | ❌ 不需要 |
学习曲线 | 陡峭 | 平缓 |
DevTools | 内置 | 需要配置 |
中间件 | 丰富 | 简单 |
包大小 | ~11KB | ~1KB |
适用场景 | 大型应用 | 中小型应用 |
6.3.3 Zustand 的完整示例
// store.js
import { create } from 'zustand'const useStore = create((set, get) => ({// Statecount: 0,users: [],// Actions (同步和异步都行!)increment: () => set((state) => ({ count: state.count + 1 })),// 异步 action - 不需要特殊 API!fetchUsers: async () => {set({ loading: true })const users = await fetch('/api/users').then((r) => r.json())set({ users, loading: false })},// 可以访问当前 stateincrementIfOdd: () => {const state = get()if (state.count % 2 === 1) {set({ count: state.count + 1 })}}
}))// 使用 - 超简单!
function Counter() {const count = useStore((state) => state.count)const increment = useStore((state) => state.increment)// 或者: const { count, increment } = useStore()return <button onClick={increment}>{count}</button>
}
6.4 选择建议
6.4.1 React 生态
小型项目 (< 10 个状态)
→ Context API + useReducer中型项目 (10-50 个状态)
→ Zustand (简单) 或 Redux Toolkit (复杂业务)大型项目 (> 50 个状态)
→ Redux Toolkit (标准化、可维护)
6.4.2 Vue 生态
Vue 2 项目
→ Vuex 3 (官方支持)Vue 3 新项目
→ Pinia (官方推荐)Vue 3 老项目
→ 逐步迁移到 Pinia
6.4.3 跨框架需求
需要 React + Vue 共享状态
→ Redux Core + 各自的绑定层需要极致性能
→ MobX (细粒度响应式)需要简单轻量
→ Zustand (React) / Pinia (Vue)
第七章:架构设计与最佳实践
7.1 文件结构组织
7.1.1 Redux Toolkit 推荐结构
src/
├── features/ # 按功能模块组织
│ ├── counter/
│ │ ├── counterSlice.js # Slice 定义
│ │ ├── Counter.jsx # 组件
│ │ └── counterSelectors.js # Selectors (可选)
│ │
│ ├── user/
│ │ ├── userSlice.js
│ │ ├── userAPI.js # API 调用
│ │ ├── UserProfile.jsx
│ │ └── userSelectors.js
│ │
│ └── posts/
│ ├── postsSlice.js
│ ├── postsAPI.js
│ ├── PostList.jsx
│ └── PostDetail.jsx
│
├── store/
│ └── index.js # 配置 store
│
├── hooks/ # 自定义 hooks
│ ├── useAuth.js
│ └── useDebounce.js
│
└── App.jsx
7.1.2 Pinia 推荐结构
src/
├── stores/ # 所有 store 文件
│ ├── counter.js
│ ├── user.js
│ └── posts.js
│
├── views/ # 页面组件
│ ├── Home.vue
│ └── Profile.vue
│
├── components/ # 公共组件
│ ├── Header.vue
│ └── Footer.vue
│
└── main.js
7.2 Slice/Store 设计原则
7.2.1 单一职责原则
// ✅ 好:每个 slice 职责单一
const userSlice = createSlice({name: 'user',initialState: { profile: null, loading: false },reducers: {setProfile: (state, action) => {state.profile = action.payload}}
})const authSlice = createSlice({name: 'auth',initialState: { token: null, isAuthenticated: false },reducers: {setToken: (state, action) => {state.token = action.payloadstate.isAuthenticated = true}}
})// ❌ 不好:职责混乱
const appSlice = createSlice({name: 'app',initialState: {user: null,posts: [],comments: [],settings: {}}// 太多不相关的状态混在一起
})
7.2.2 规范化状态结构
// ❌ 不好:嵌套的数据结构
const state = {posts: [{id: 1,title: 'Post 1',author: { id: 1, name: 'John' },comments: [{ id: 1, text: 'Comment 1', author: { id: 2, name: 'Jane' } }]}]
}// ✅ 好:规范化的数据结构
const state = {posts: {ids: [1],entities: {1: { id: 1, title: 'Post 1', authorId: 1, commentIds: [1] }}},users: {ids: [1, 2],entities: {1: { id: 1, name: 'John' },2: { id: 2, name: 'Jane' }}},comments: {ids: [1],entities: {1: { id: 1, text: 'Comment 1', authorId: 2 }}}
}// RTK 提供了 createEntityAdapter 简化规范化
import { createEntityAdapter } from '@reduxjs/toolkit'const postsAdapter = createEntityAdapter()const postsSlice = createSlice({name: 'posts',initialState: postsAdapter.getInitialState(),reducers: {addPost: postsAdapter.addOne,addPosts: postsAdapter.addMany,updatePost: postsAdapter.updateOne,removePost: postsAdapter.removeOne}
})// 自动生成的 selectors
const {selectAll: selectAllPosts,selectById: selectPostById,selectIds: selectPostIds
} = postsAdapter.getSelectors((state) => state.posts)
7.3 异步数据管理模式
7.3.1 统一的异步状态模板
// asyncState.js - 可复用的模板
export const createAsyncState = () => ({data: null,loading: false,error: null,lastFetched: null
})export const createAsyncReducers = (builder, asyncThunk, stateKey = 'data') => {builder.addCase(asyncThunk.pending, (state) => {state.loading = truestate.error = null}).addCase(asyncThunk.fulfilled, (state, action) => {state.loading = falsestate[stateKey] = action.payloadstate.lastFetched = Date.now()}).addCase(asyncThunk.rejected, (state, action) => {state.loading = falsestate.error = action.error.message})
}// 使用
const userSlice = createSlice({name: 'user',initialState: {profile: createAsyncState(),posts: createAsyncState()},reducers: {},extraReducers: (builder) => {createAsyncReducers(builder, fetchUserProfile, 'profile')createAsyncReducers(builder, fetchUserPosts, 'posts')}
})
7.3.2 请求缓存策略
// cacheMiddleware.js
const createCacheMiddleware = (cacheTime = 5 * 60 * 1000) => {return (store) => (next) => (action) => {// 只处理 pending 的异步请求if (action.type.endsWith('/pending')) {const state = store.getState()const sliceName = action.type.split('/')[0]const sliceState = state[sliceName]// 检查缓存是否有效if (sliceState.data && Date.now() - sliceState.lastFetched < cacheTime) {console.log('Using cached data')return // 跳过请求}}return next(action)}
}// 应用中间件
const store = configureStore({reducer: rootReducer,middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(createCacheMiddleware())
})
7.4 性能优化最佳实践
7.4.1 Selector 优化
// ❌ 不好:组件内创建 selector
function UserList() {const activeUsers = useSelector((state) => state.users.filter((user) => user.active) // 每次渲染都重新过滤)
}// ✅ 好:使用 reselect
const selectActiveUsers = createSelector([(state) => state.users],(users) => users.filter((user) => user.active) // 缓存结果
)function UserList() {const activeUsers = useSelector(selectActiveUsers)
}
7.4.2 批量更新
// ❌ 不好:多次 dispatch
dispatch(addPost(post1))
dispatch(addPost(post2))
dispatch(addPost(post3))
// 触发 3 次重渲染// ✅ 好:批量更新
import { batch } from 'react-redux'batch(() => {dispatch(addPost(post1))dispatch(addPost(post2))dispatch(addPost(post3))
})
// 只触发 1 次重渲染// ✅ 更好:使用 addMany
dispatch(addPosts([post1, post2, post3]))
7.4.3 组件粒度控制
// ❌ 不好:一个大组件订阅所有数据
function Dashboard() {const { users, posts, comments, settings } = useSelector(state => ({users: state.users,posts: state.posts,comments: state.comments,settings: state.settings}))return (<div><UserList users={users} /><PostList posts={posts} /><CommentList comments={comments} /><Settings settings={settings} /></div>)
}// ✅ 好:每个子组件各自订阅
function Dashboard() {return (<div><UserList /> {/* 内部订阅 users */}<PostList /> {/* 内部订阅 posts */}<CommentList /> {/* 内部订阅 comments */}<Settings /> {/* 内部订阅 settings */}</div>)
}function UserList() {const users = useSelector(state => state.users) // 只订阅需要的数据return <div>{users.map(...)}</div>
}
7.5 错误处理与日志
7.5.1 全局错误处理
// errorMiddleware.js
const errorMiddleware = (store) => (next) => (action) => {try {return next(action)} catch (error) {console.error('Action Error:', error)// 记录到错误监控服务if (typeof window !== 'undefined' && window.Sentry) {window.Sentry.captureException(error)}// Dispatch 错误 actionstore.dispatch({type: 'app/error',payload: {message: error.message,stack: error.stack,action: action.type}})}
}
7.5.2 异步错误处理
// 完善的 asyncThunk 错误处理
const fetchUser = createAsyncThunk('user/fetch', async (userId, { rejectWithValue }) => {try {const response = await fetch(`/api/users/${userId}`)if (!response.ok) {// 处理 HTTP 错误const error = await response.json()return rejectWithValue({status: response.status,message: error.message || 'Failed to fetch user'})}return response.json()} catch (error) {// 处理网络错误if (error.name === 'AbortError') {return rejectWithValue({ message: 'Request cancelled' })}return rejectWithValue({message: error.message || 'Network error'})}
})// 在 slice 中处理
extraReducers: (builder) => {builder.addCase(fetchUser.rejected, (state, action) => {state.loading = falseif (action.payload) {// 自定义错误state.error = action.payload.message// 根据错误类型处理if (action.payload.status === 404) {state.userNotFound = true}} else {// 未处理的错误state.error = 'An unexpected error occurred'}})
}
7.6 测试策略
7.6.1 Slice 测试
// counterSlice.test.js
import counterReducer, { increment, decrement } from './counterSlice'describe('counterSlice', () => {const initialState = { value: 0 }it('should handle initial state', () => {expect(counterReducer(undefined, { type: 'unknown' })).toEqual({value: 0})})it('should handle increment', () => {const actual = counterReducer(initialState, increment())expect(actual.value).toEqual(1)})it('should handle decrement', () => {const actual = counterReducer({ value: 1 }, decrement())expect(actual.value).toEqual(0)})
})
7.6.2 AsyncThunk 测试
// userSlice.test.js
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import { fetchUser } from './userSlice'const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)describe('fetchUser', () => {it('should fetch user successfully', async () => {global.fetch = jest.fn(() =>Promise.resolve({ok: true,json: () => Promise.resolve({ id: 1, name: 'John' })}))const store = mockStore({ user: {} })await store.dispatch(fetchUser(1))const actions = store.getActions()expect(actions[0].type).toBe('user/fetchUser/pending')expect(actions[1].type).toBe('user/fetchUser/fulfilled')expect(actions[1].payload).toEqual({ id: 1, name: 'John' })})it('should handle error', async () => {global.fetch = jest.fn(() => Promise.reject(new Error('Network error')))const store = mockStore({ user: {} })await store.dispatch(fetchUser(1))const actions = store.getActions()expect(actions[1].type).toBe('user/fetchUser/rejected')expect(actions[1].error.message).toBe('Network error')})
})
7.6.3 组件测试
// Counter.test.jsx
import { render, fireEvent } from '@testing-library/react'
import { Provider } from 'react-redux'
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
import Counter from './Counter'describe('Counter', () => {let storebeforeEach(() => {store = configureStore({reducer: { counter: counterReducer }})})it('should display current count', () => {const { getByText } = render(<Provider store={store}><Counter /></Provider>)expect(getByText(/count: 0/i)).toBeInTheDocument()})it('should increment on button click', () => {const { getByText } = render(<Provider store={store}><Counter /></Provider>)fireEvent.click(getByText(/\+1/i))expect(getByText(/count: 1/i)).toBeInTheDocument()})
})
7.7 TypeScript 集成
7.7.1 RTK 的 TypeScript 配置
// store.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter/counterSlice'export const store = configureStore({reducer: {counter: counterReducer}
})// 导出类型
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch// hooks.ts - 类型化的 hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
7.7.2 Slice 的 TypeScript
// counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'interface CounterState {value: numberstatus: 'idle' | 'loading' | 'failed'
}const initialState: CounterState = {value: 0,status: 'idle'
}const counterSlice = createSlice({name: 'counter',initialState,reducers: {increment: (state) => {state.value += 1},incrementByAmount: (state, action: PayloadAction<number>) => {state.value += action.payload}}
})export const { increment, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
7.7.3 AsyncThunk 的 TypeScript
// userSlice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import type { RootState } from '../store'interface User {id: numbername: stringemail: string
}interface UserState {data: User | nullloading: booleanerror: string | null
}export const fetchUser = createAsyncThunk<User, // 返回类型number, // 参数类型{ state: RootState; rejectValue: string } // ThunkAPI 类型
>('user/fetchUser', async (userId, { rejectWithValue }) => {try {const response = await fetch(`/api/users/${userId}`)if (!response.ok) {return rejectWithValue('Failed to fetch user')}return response.json()} catch (error) {return rejectWithValue((error as Error).message)}
})const initialState: UserState = {data: null,loading: false,error: null
}const userSlice = createSlice({name: 'user',initialState,reducers: {},extraReducers: (builder) => {builder.addCase(fetchUser.pending, (state) => {state.loading = true}).addCase(fetchUser.fulfilled, (state, action) => {state.loading = falsestate.data = action.payload}).addCase(fetchUser.rejected, (state, action) => {state.loading = falsestate.error = action.payload ?? 'Unknown error'})}
})export default userSlice.reducer
附录:技术选型决策树
需要状态管理?
├─ 是
│ ├─ 使用 React?
│ │ ├─ 简单项目 → Context API + useReducer
│ │ ├─ 中型项目 → Zustand
│ │ └─ 大型/复杂项目 → Redux Toolkit
│ │
│ └─ 使用 Vue?
│ ├─ Vue 2 → Vuex 3
│ └─ Vue 3 → Pinia
│
└─ 否 → 本地状态 (useState/ref)
总结
本文档系统地介绍了现代前端状态管理的方方面面:
- Redux 生态:从传统 Redux 到 Redux Toolkit 的演进,理解核心概念和最佳实践
- Vue 状态管理:Vuex 到 Pinia 的演变,理解响应式状态管理
- React vs Vue:对比两大框架的响应式机制和设计哲学
- 异步管理:深入理解 Thunk、createAsyncThunk 的工作原理
- 性能优化:掌握 useSelector、Reselect、useMemo 等优化技术
- 跨框架方案:了解框架无关的状态管理库
- 架构实践:文件组织、测试策略、TypeScript 集成等生产实践
核心要点:
- Redux Toolkit 是 Redux 的标准方式,大幅简化开发体验
- Pinia 是 Vue 3 的官方推荐,统一了同步和异步处理
- 性能优化的关键是理解引用比较和选择性订阅
- 异步操作需要特殊处理,但现代工具已经大大简化了流程
- 选择合适的工具比追求完美更重要
学习建议:
- 新手:直接学 Redux Toolkit 或 Pinia,不要纠结传统写法
- 进阶:理解底层原理,掌握性能优化技巧
- 实践:在真实项目中应用,积累经验
- 持续:关注生态演进,及时采用最佳实践
文档信息
- 文档完成时间:2025年10月
- 版本:v1.0