【前端状态管理技术解析:Redux 与 Vue 生态对比】

前端状态管理技术解析:Redux 与 Vue 生态对比

基于 React、Vue 生态的技术分析与实践指南

目录


第一章:Redux 生态系统

1.1 Redux 的核心概念

Redux 是一个可预测的 JavaScript 状态容器,它通过三个核心要素管理应用状态:

javascript 复制代码
// Redux 的数据流
┌─────────────────────────────────────────┐
│              Redux Store                │
│         (整个应用的状态树)              │
└─────────────────────────────────────────┘
                    ▲
                    │
         ┌──────────┴──────────┐
         │                     │
    ┌────┴────┐          ┌─────┴─────┐
    │ Action  │          │  Reducer  │
    │ (做什么)│          │ (怎么做)  │
    └─────────┘          └───────────┘

核心原则:

  1. 单一数据源:整个应用的 state 存储在一个 store 中
  2. State 是只读的:只能通过 dispatch action 来修改
  3. 使用纯函数进行修改: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') // 正确方式

核心原因:

  1. 可追踪性:DevTools 可以记录每个 mutation
  2. 时间旅行调试:可以回放状态变化
  3. 明确的数据流:清楚知道谁修改了状态
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?
  1. 简化 API:Mutation 和 Action 的区分让新手困惑
  2. TypeScript 支持更好:减少样板代码
  3. 现代 DevTools:新的调试工具可以追踪 action 内的所有变化
  4. 对齐其他库:与 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 必须是纯函数的原因:

  1. 可预测性:相同输入必定产生相同输出
  2. 可测试性:易于单元测试
  3. 可追踪性:DevTools 可以记录每个状态变化
  4. 时间旅行:可以回放历史状态
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)

总结

本文档系统地介绍了现代前端状态管理的方方面面:

  1. Redux 生态:从传统 Redux 到 Redux Toolkit 的演进,理解核心概念和最佳实践
  2. Vue 状态管理:Vuex 到 Pinia 的演变,理解响应式状态管理
  3. React vs Vue:对比两大框架的响应式机制和设计哲学
  4. 异步管理:深入理解 Thunk、createAsyncThunk 的工作原理
  5. 性能优化:掌握 useSelector、Reselect、useMemo 等优化技术
  6. 跨框架方案:了解框架无关的状态管理库
  7. 架构实践:文件组织、测试策略、TypeScript 集成等生产实践

核心要点:

  • Redux Toolkit 是 Redux 的标准方式,大幅简化开发体验
  • Pinia 是 Vue 3 的官方推荐,统一了同步和异步处理
  • 性能优化的关键是理解引用比较和选择性订阅
  • 异步操作需要特殊处理,但现代工具已经大大简化了流程
  • 选择合适的工具比追求完美更重要

学习建议:

  1. 新手:直接学 Redux Toolkit 或 Pinia,不要纠结传统写法
  2. 进阶:理解底层原理,掌握性能优化技巧
  3. 实践:在真实项目中应用,积累经验
  4. 持续:关注生态演进,及时采用最佳实践


文档信息

  • 文档完成时间:2025年10月
  • 版本:v1.0
相关推荐
一 乐16 小时前
医疗保健|医疗养老|基于Java+vue的医疗保健系统(源码+数据库+文档)
java·前端·数据库·vue.js·毕设
Want59516 小时前
HTML炫酷烟花⑨
前端·html
艾小码16 小时前
90%前端面试必问的12个JS核心,搞懂这些直接起飞!
前端·javascript
qq_5470261791 天前
Flowable 工作流引擎
java·服务器·前端
刘逸潇20051 天前
CSS基础语法
前端·css
Sheldon一蓑烟雨任平生1 天前
Vue3 插件(可选独立模块复用)
vue.js·vue3·插件·vue3 插件·可选独立模块·插件使用方式·插件中的依赖注入
吃饺子不吃馅1 天前
[开源] 从零到一打造在线 PPT 编辑器:React + Zustand + Zundo
前端·svg·图形学
小马哥编程1 天前
【软考架构】案例分析-Web应用设计(应用服务器概念)
前端·架构
鱼与宇1 天前
苍穹外卖-VUE
前端·javascript·vue.js
啃火龙果的兔子1 天前
前端直接渲染Markdown
前端