前端状态管理技术解析:Redux 与 Vue 生态对比
基于 React、Vue 生态的技术分析与实践指南
目录
- [第一章:Redux 生态系统](#第一章:Redux 生态系统)
- [第二章:Vue 状态管理演进](#第二章:Vue 状态管理演进)
- [第三章:React vs Vue 响应式机制对比](#第三章:React vs Vue 响应式机制对比)
- 第四章:异步状态管理
- 第五章:性能优化与缓存机制
- 第六章:跨框架状态管理
- 第七章:架构设计与最佳实践
第一章:Redux 生态系统
1.1 Redux 的核心概念
Redux 是一个可预测的 JavaScript 状态容器,它通过三个核心要素管理应用状态:
javascript
// Redux 的数据流
┌─────────────────────────────────────────┐
│ Redux Store │
│ (整个应用的状态树) │
└─────────────────────────────────────────┘
▲
│
┌──────────┴──────────┐
│ │
┌────┴────┐ ┌─────┴─────┐
│ Action │ │ Reducer │
│ (做什么)│ │ (怎么做) │
└─────────┘ └───────────┘
核心原则:
- 单一数据源:整个应用的 state 存储在一个 store 中
- State 是只读的:只能通过 dispatch action 来修改
- 使用纯函数进行修改:Reducer 必须是纯函数
1.2 传统 Redux 的完整实现
1.2.1 定义 Action Types
javascript
// actionTypes.js
// 为什么需要?避免字符串拼写错误,提供类型检查
export const INCREMENT = 'counter/INCREMENT'
export const DECREMENT = 'counter/DECREMENT'
export const ADD_USER = 'user/ADD_USER'
1.2.2 创建 Action Creators
javascript
// 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
javascript
// 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 组合模块
javascript
// 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.counter
user: userReducer // 管理 state.user
})
// 组合后的 state 结构:
// {
// counter: { value: 0 },
// user: { users: [] }
// }
export default rootReducer
combineReducers 的工作原理:
javascript
// 简化版实现
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 和中间件
javascript
// 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 中使用
javascript
// 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() {
// 读取 state
const 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 安装依赖
bash
# 只需要安装两个包
npm install @reduxjs/toolkit react-redux
# @reduxjs/toolkit 自动包含:
# - redux (核心库)
# - immer (处理不可变更新)
# - redux-thunk (异步中间件)
# - reselect (性能优化)
1.3.2 使用 createSlice 创建模块
javascript
// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit'
// createSlice 自动完成:
// 1. 创建 action types
// 2. 创建 action creators
// 3. 创建 reducer
const 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 的魔法原理:
javascript
// createSlice 内部做了什么(简化版)
const counterSlice = {
// 自动生成的 action creators
actions: {
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 配置
javascript
// 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, // 自动 combineReducers
user: userReducer
}
})
export default store
1.3.4 代码量对比
传统 Redux:约 60 行代码
javascript
// actionTypes.js (5 行)
// actions.js (15 行)
// reducer.js (20 行)
// store.js (10 行)
// 总计:~50-60 行
Redux Toolkit:约 20 行代码
javascript
// counterSlice.js (15 行)
// store.js (5 行)
// 总计:~20 行,减少了 67%!
1.4 Redux 生态系统的组成
javascript
// 库的依赖关系
@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 为什么需要不可变数据?
javascript
// 可预测性
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 的手动不可变更新
javascript
// ❌ 错误:直接修改
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 集成
javascript
// 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 的工作原理:
javascript
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 有不同的设计哲学。
javascript
┌─────────────────────────────────────────┐
│ Vuex Store │
├─────────────────────────────────────────┤
│ State (状态) │
│ ↓ │
│ Getters (计算属性,可选) │
│ ↓ │
│ Mutations (同步修改,必须) │
│ ↓ │
│ Actions (异步操作,可选) │
└─────────────────────────────────────────┘
2.2 Vuex 的完整示例
javascript
// 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 必须是同步的,异步操作放在 Actions
actions: {
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?
javascript
// ❌ Vuex 不允许直接修改 state
this.$store.state.count++ // 违反规则!虽然能工作,但不推荐
// ✅ 必须通过 mutation
this.$store.commit('INCREMENT') // 正确方式
核心原因:
- 可追踪性:DevTools 可以记录每个 mutation
- 时间旅行调试:可以回放状态变化
- 明确的数据流:清楚知道谁修改了状态
2.3.2 Mutation 的严格规则
javascript
mutations: {
// ✅ 同步操作
INCREMENT(state) {
state.count++
},
// ❌ 不能包含异步操作!
ASYNC_INCREMENT(state) {
setTimeout(() => {
state.count++ // 违反规则!DevTools 无法追踪
}, 1000)
}
}
actions: {
// ✅ 异步操作放在 action
asyncIncrement({ commit }) {
setTimeout(() => {
commit('INCREMENT') // 提交同步 mutation
}, 1000)
}
}
2.4 Vuex 模块化
2.4.1 文件结构模块化
javascript
// 推荐的文件结构
store/
├── index.js # 组装模块并导出 store
├── modules/
│ ├── counter.js # counter 模块
│ ├── user.js # user 模块
│ └── todos.js # todos 模块
└── getters.js # 根级别的 getters(可选)
2.4.2 命名空间模块
javascript
// 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.counter
user // 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 的简化设计
javascript
// store.js (Pinia)
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// State
state: () => ({
count: 0,
users: []
}),
// Getters (同 Vuex)
getters: {
doubleCount: (state) => state.count * 2
},
// Actions - 同时处理同步和异步!
// 不再区分 mutations 和 actions
actions: {
// 同步操作
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 等保持一致
javascript
// Vuex 3/4 - 需要区分
mutations: { INCREMENT(state) { state.count++ } }
actions: { async fetch() { /* ... */ } }
// Pinia - 统一到 actions
actions: {
increment() { this.count++ }, // 同步
async fetch() { /* ... */ } // 异步
}
2.5.3 Pinia 的使用
javascript
// 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 的手动追踪
javascript
// React: 显式更新,手动告诉框架"我要更新了"
function Counter() {
const [count, setCount] = useState(0)
// ❌ 不会触发重渲染
count = count + 1
// ✅ 必须调用 setter
setCount(count + 1)
}
// 设计哲学:
// UI = f(state) // 纯函数
// 新 state → 返回新 UI
3.1.2 Vue 的自动追踪
javascript
// Vue: 隐式更新,自动知道"你更新了"
const count = ref(0)
count.value++ // Vue 自动检测到变化并重新渲染
// 设计哲学:
// 响应式代理 → 自动追踪依赖 → 自动更新
3.2 响应式系统对比
3.2.1 React 的不可变更新
javascript
// 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 的可变更新
javascript
// 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 的自顶向下更新
javascript
// React - 父组件更新,子组件也重新执行
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<Child /> {/* Parent 更新,Child 也重新执行 */}
</div>
)
}
// 需要手动优化
const MemoChild = React.memo(Child) // 手动优化
3.3.2 Vue 的精确更新
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 的函数式风格
javascript
// 类组件时代 - 高阶组件(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 的声明式风格
javascript
// Vue 2 - 选项式 API
data() {
return { count: 0 }
}
this.count++ // 直接修改
// Vue 3 - 组合式 API(更函数式)
const count = ref(0)
count.value++ // 仍然是赋值
第四章:异步状态管理
4.1 为什么需要异步处理?
4.1.1 核心问题:Reducer 必须是纯函数
javascript
// ❌ 如果允许在 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 完整的异步操作需要三个状态
javascript
// 一个异步请求有三种可能的结果:
{
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 的数据流
javascript
// Redux 的中间件机制
dispatch(action) → [中间件] → reducer → new state
↑
异步操作在这里!
4.2.2 Redux Thunk 的原理
javascript
// 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 中间件的实现(非常简单!):
javascript
// 完整的 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
javascript
// 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:
javascript
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false },
// reducers: 处理内部 actions
// 自动生成 action creators: user/setName
reducers: {
setName: (state, action) => {
state.data.name = action.payload
}
// 这里只能处理 "user/xxx" 的 actions
},
// extraReducers: 处理外部 actions
// 处理来自 createAsyncThunk 的 actions
extraReducers: (builder) => {
// 处理 "user/fetch/pending", "user/fetch/fulfilled" 等
builder.addCase(fetchUser.fulfilled, (state, action) => {
state.data = action.payload
})
}
})
概念关系图:
createAsyncThunk → 自动生成 3 个 action types
↓
user/fetch/pending
user/fetch/fulfilled
user/fetch/rejected
↓
extraReducers → 处理这些外部 actions
4.3.2 完整的 AsyncThunk 实现
javascript
// 第 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
// 可以访问当前 state
const 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: {
// 同步 actions
clearUser: (state) => {
state.data = null
}
},
extraReducers: (builder) => {
// builder.addCase = "当收到这个 action 时,执行这个函数"
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false
state.data = action.payload // payload 是 async 函数的返回值
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false
state.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 返回一个 Promise
dispatch(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 执行时序详解
javascript
// 问题:useEffect 和 loading 判断是同步执行的吗?
function UserProfile() {
const { loading } = useSelector((state) => state.user) // 初始: false
const 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
javascript
// 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
javascript
// Pinia 简化了!不需要 mutation
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
loading: false,
error: null
}),
actions: {
// Action 可以直接修改 state,可以异步!
async fetchUser(userId) {
this.loading = true
this.error = null
try {
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 必须立即返回 → 不能异步 → 需要 Thunk
Pinia 的自由:
Action 不需要立即返回 → 可以异步 → Vue 响应式自动追踪每次修改
4.5.2 技术原因对比
Redux 为什么不能异步?
javascript
// Redux reducer 的要求
function reducer(state, action) {
// 必须立即返回新 state
return newState // 不能是 Promise!
}
// 如果允许异步:
function reducer(state, action) {
fetch('/api').then((data) => {
return { ...state, data } // ❌ 太晚了!
})
return state // 必须现在就返回
}
Pinia 为什么可以异步?
javascript
// 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 统一的异步状态结构
javascript
// 推荐的异步状态结构
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 取消请求
javascript
// 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 请求去重
javascript
// 避免重复请求
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 的来源
javascript
// 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 内使用
javascript
// 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 获取 store
if (!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 默认使用严格相等比较(===)
javascript
// 问题场景:返回新对象
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('渲染了') // 会疯狂打印!
}
为什么每次都是新对象?
javascript
// 每次调用函数都创建新对象
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:分开选择(推荐)
javascript
// ✅ 每个值单独选择
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
javascript
// ✅ 浅层比较对象的每个属性
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 缓存
javascript
// ✅ 使用 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 中选择数据的函数。
javascript
// 这就是一个 selector 函数
const selectCount = (state) => state.counter.count
// ↑ 函数名 ↑ 从整个 state 中选择 count
// 使用
const count = useSelector(selectCount)
5.3.2 什么是 Reselect?
Reselect 创建带缓存的 selector,避免重复计算。
javascript
// 问题:没有缓存的昂贵计算
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 的工作原理
javascript
// 简化版的 createSelector 实现
function createSelector(inputSelectors, resultFunc) {
let lastInputs = []
let lastResult
return (state) => {
// 1. 获取当前输入
const currentInputs = inputSelectors.map((selector) => selector(state))
// 2. 比较输入是否变化(浅比较)
const inputsChanged = currentInputs.some((input, index) => input !== lastInputs[index])
// 3. 只有变化时才重新计算
if (inputsChanged) {
lastInputs = currentInputs
lastResult = resultFunc(...currentInputs)
}
// 4. 返回结果(可能是缓存的)
return lastResult
}
}
5.3.5 Reselect 的存储位置
缓存存储在函数闭包中,全局有效:
javascript
// 每个 selector 实例都有自己的闭包
const selectTotalPrice = createSelector(...) // 闭包 1
const selectTotalTax = createSelector(...) // 闭包 2
function 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 的内存管理
默认只缓存最后一次的结果:
javascript
const selectExpensiveData = createSelector(...)
// 第 1 次调用
selectExpensiveData(state1) // lastInputs = [...], lastResult = result1
// 第 2 次调用:输入变化
selectExpensiveData(state2) // lastInputs = [...], lastResult = result2
// result1 被覆盖,可以被垃圾回收!
// 所以不会无限累积内存!
内存风险场景:
javascript
// ❌ 在组件中动态创建 selector
function UserComponent({ userId }) {
// 每次渲染都创建新的 selector!
const selectUser = createSelector([(state) => state.users], (users) =>
users.find((u) => u.id === userId)
)
// 如果这个组件被创建很多次,会有很多 selector 实例
}
// ✅ 正确:在模块级别定义
const selectUsers = (state) => state.users
function UserComponent({ userId }) {
// 使用 useMemo 管理动态 selector
const 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 核心相似点
都是计算属性 + 自动缓存:
javascript
// 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 缓存机制对比
javascript
// Reselect: 手动实现的缓存
function createSelector(inputSelectors, resultFunc) {
let lastArgs = null
let lastResult = null
return (state) => {
const newArgs = inputSelectors.map((selector) => selector(state))
// 手动比较参数是否变化
if (lastArgs === null || argsChanged(newArgs, lastArgs)) {
lastArgs = newArgs
lastResult = 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 核心区别
javascript
// 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?
javascript
// ❌ 不能用 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 组合使用
javascript
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 的存储
javascript
// 存储位置: 组件实例的 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 的存储
javascript
// 存储位置: 组件实例的 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 的存储
javascript
// 存储位置: 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 的存储
javascript
// 存储位置: 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 的跨框架使用
javascript
// 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 的跨框架能力
javascript
// store.js - MobX,框架无关
import { makeObservable, observable, action } from 'mobx'
class CounterStore {
count = 0
constructor() {
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 响应式系统
javascript
// Pinia 内部使用 Vue 的 reactive 和 computed
import { reactive, computed } from 'vue' // 必须依赖 Vue!
export function defineStore(name, options) {
const state = reactive(options.state()) // Vue 的 reactive
const getters = computed(() => ...) // Vue 的 computed
// 无法在 React 中用!
}
6.2.2 设计初衷
Redux Core:
- 设计目标:通用状态管理
- 核心:纯 JavaScript 对象 + 纯函数
- 可以在任何环境使用
Pinia/Vuex:
- 设计目标:Vue 专用状态管理
- 核心:Vue 响应式系统
- 深度集成 Vue 生态
6.3 Zustand:轻量级的状态管理
6.3.1 Zustand 的特点
javascript
// 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 的完整示例
javascript
// store.js
import { create } from 'zustand'
const useStore = create((set, get) => ({
// State
count: 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 })
},
// 可以访问当前 state
incrementIfOdd: () => {
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 单一职责原则
javascript
// ✅ 好:每个 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.payload
state.isAuthenticated = true
}
}
})
// ❌ 不好:职责混乱
const appSlice = createSlice({
name: 'app',
initialState: {
user: null,
posts: [],
comments: [],
settings: {}
}
// 太多不相关的状态混在一起
})
7.2.2 规范化状态结构
javascript
// ❌ 不好:嵌套的数据结构
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 统一的异步状态模板
javascript
// 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 = true
state.error = null
})
.addCase(asyncThunk.fulfilled, (state, action) => {
state.loading = false
state[stateKey] = action.payload
state.lastFetched = Date.now()
})
.addCase(asyncThunk.rejected, (state, action) => {
state.loading = false
state.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 请求缓存策略
javascript
// 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 优化
javascript
// ❌ 不好:组件内创建 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 批量更新
javascript
// ❌ 不好:多次 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 组件粒度控制
javascript
// ❌ 不好:一个大组件订阅所有数据
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 全局错误处理
javascript
// 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 错误 action
store.dispatch({
type: 'app/error',
payload: {
message: error.message,
stack: error.stack,
action: action.type
}
})
}
}
7.5.2 异步错误处理
javascript
// 完善的 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 = false
if (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 测试
javascript
// 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 测试
javascript
// 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 组件测试
javascript
// 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 store
beforeEach(() => {
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 配置
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
typescript
// counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
status: '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
typescript
// userSlice.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import type { RootState } from '../store'
interface User {
id: number
name: string
email: string
}
interface UserState {
data: User | null
loading: boolean
error: 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 = false
state.data = action.payload
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false
state.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