Files
test-repo/docs/zh-cn/appendix/component-state-management.md
T
sanbuphy 7c70c37072 feat(docs): add interactive demo components for technical appendices
Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation.

Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
2026-02-06 03:34:50 +08:00

44 KiB
Raw Blame History

组件化与状态管理模式总览

学习指南:组件化解决的是"代码该怎么拆",状态管理解决的是"数据该怎么流"。本章节会围绕一个问题展开:当应用规模越来越大,组件之间该如何优雅地共享和同步数据?

在开始之前,建议你先补两块"基础砖":

  • Vue/React 基础组件:如果你还不熟悉组件的 props、events、生命周期等概念,建议先回顾一下 前端框架入门
  • JavaScript 模块化:了解 ES Module 和 CommonJS 的基本用法,有助于理解状态管理库的设计哲学。

0. 引言:从"一盘散沙"到"井然有序"

很多前端开发者在项目初期都会经历这样的阶段:

  • 页面功能少的时候,数据直接放组件里,props 传来传去还能应付;
  • 业务复杂了,组件层级越来越深,props drilling(属性钻取)像打地鼠一样令人崩溃;
  • 两个不相干的组件需要共享同一份数据,开始搞事件总线、全局变量,结果代码像意大利面一样纠缠不清。

直觉上,我们会以为是:"这个框架不够强大"。 但大多数时候,问题并不在于工具,而在于我们没有设计好组件的职责边界和数据流向

面对这些挑战,单纯依靠"写更多代码"已经捉襟见肘。我们需要一套系统的方法论,来在复杂的组件树中优雅地管理共享状态。这正是组件化与状态管理试图解决的问题。


1. 什么是"组件化思维"?(定义 + 场景)

先给一个简短的工作定义,再看几个典型场景。

组件化思维,是一种将用户界面拆分为独立、可复用、职责单一的代码单元的工程方法,每个组件封装自己的结构(HTML)、表现(CSS)和行为(JS),并通过明确的接口与其他组件通信。

你可以简单地把它理解成三件事:高内聚、低耦合、可复用。 常见会用到它的场景包括:

  • UI 组件库开发(如 Element Plus、Ant Design
  • **大型单页应用(SPA)**的页面拆分
  • 跨项目复用的业务组件封装

接下来,我们就从一个真实团队的"血泪教训"出发,看看他们是怎么一点点从"面条代码"进化到"组件化架构"的。


2. 从"血泪教训"说起:某电商团队的组件化重构

2.1 初始阶段:一团乱麻的"大泥球"

某电商团队在 2019 年启动了一个促销活动后台管理系统。初期为了快速上线,采用了传统的 jQuery + 服务端渲染模式。

代码结构大概长这样:

<!-- promotion-edit.html -->
<div id="page">
  <div class="header">...</div>

  <!-- 活动基本信息表单 -->
  <form id="basic-info">
    <input name="activityName" />
    <input name="startTime" type="datetime-local" />
    <!-- 200+ 行的表单字段... -->
  </form>

  <!-- 商品选择弹窗(隐藏在页面里) -->
  <div id="product-modal" style="display:none">
    <!-- 商品列表、搜索、分页... -->
  </div>

  <!-- 优惠券规则配置(另一个弹窗) -->
  <div id="coupon-modal" style="display:none">
    <!-- 复杂的规则配置表单... -->
  </div>

  <script>
    // 1000+ 行的 jQuery 代码,处理各种交互
    $(function() {
      // 初始化日期选择器
      $('#startTime').datetimepicker({...});

      // 打开商品弹窗
      $('#select-product-btn').click(function() {
        $('#product-modal').show();
        loadProductList();
      });

      // 提交表单(200+ 行的表单验证和提交逻辑)
      $('#submit-btn').click(function() {
        // ...
      });
    });
  </script>
</div>

当时的痛点:

问题 表现 影响
代码冗余 商品选择弹窗在 5 个页面复制粘贴 修改一次要改 5 处,经常漏改
耦合严重 表单验证逻辑散落在 HTML、JS、CSS 中 改一个字段可能引发连锁 Bug
难以测试 所有逻辑都挂在 jQuery 回调里 无法单元测试,只能靠人工点
团队协作难 5 个前端同时改一个文件 Git 冲突频发,合并痛苦

2.2 第一次重构:Vue 2 的曙光

2020 年,团队决定用 Vue 2 重构系统。这次重构的核心目标是:按页面维度拆分组件

重构后的代码结构:

<!-- ActivityEdit.vue -->
<template>
  <div class="activity-edit">
    <PageHeader title="编辑活动" />

    <BasicInfoForm v-model="activityData" />

    <ProductSelector
      v-model="activityData.productIds"
      @open="showProductModal = true"
    />

    <CouponRules
      v-model="activityData.coupons"
      @open="showCouponModal = true"
    />

    <div class="actions">
      <el-button @click="save">保存</el-button>
      <el-button @click="publish">发布</el-button>
    </div>

    <!-- 弹窗组件 -->
    <ProductModal
      v-model:visible="showProductModal"
      @select="onProductSelect"
    />
    <CouponModal
      v-model:visible="showCouponModal"
      @confirm="onCouponConfirm"
    />
  </div>
</template>

<script>
export default {
  components: {
    PageHeader,
    BasicInfoForm,
    ProductSelector,
    CouponRules,
    ProductModal,
    CouponModal
  },
  data() {
    return {
      activityData: {
        name: '',
        startTime: null,
        productIds: [],
        coupons: []
      },
      showProductModal: false,
      showCouponModal: false
    }
  },
  methods: {
    save() {
      // 保存逻辑
    },
    publish() {
      // 发布逻辑
    }
  }
}
</script>

这次重构带来的改变:

  • 组件复用:商品选择弹窗从 5 个页面复制粘贴,变成了 1 个 <ProductModal> 组件到处复用
  • 职责分离:每个组件只负责自己的逻辑,表单验证拆到 <BasicInfoForm> 内部
  • 团队协作:5 个前端可以并行开发不同组件,Git 冲突大幅减少

但很快,新的问题又出现了...

2.3 第二次重构:状态管理的觉醒

随着业务复杂度增加,组件之间的通信变得越来越复杂。

典型场景:购物车状态同步

用户在一个页面把商品加入购物车,购物车图标上的数字要实时更新。但购物车状态散落在多个组件中:

<!-- Header.vue -->
<template>
  <header>
    <div class="cart-icon">
      <i class="icon-cart"></i>
      <span class="badge">{{ cartCount }}</span>
    </div>
  </header>
</template>

<script>
export default {
  data() {
    return {
      cartCount: 0  // 自己的购物车数量
    }
  },
  created() {
    // 方案1:事件总线
    this.$bus.$on('cart-updated', (count) => {
      this.cartCount = count
    })

    // 方案2localStorage 轮询
    setInterval(() => {
      this.cartCount = JSON.parse(localStorage.getItem('cart')).length
    }, 1000)
  }
}
</script>

问题爆发:

反模式 代码表现 后果
事件总线地狱 $bus.$emit 满天飞,不知道谁监听谁 调试困难,内存泄漏
Props Drilling 数据层层传递,中间组件只是"搬运工" 中间组件被迫耦合
LocalStorage 滥用 把状态存 localStorage 然后轮询 性能差,数据不一致
全局变量 window.sharedState 直接修改 无法追踪变化,Bug 难定位

最终解决方案:Vuex + 组件化设计规范

团队引入了 Vuex 作为集中式状态管理,并制定了组件设计规范:

// store/modules/cart.js
export default {
  namespaced: true,
  state: {
    items: [],
    selectedIds: []
  },
  getters: {
    totalCount: state => state.items.reduce((sum, item) => sum + item.quantity, 0),
    totalPrice: state => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  },
  mutations: {
    ADD_ITEM(state, item) {
      const existing = state.items.find(i => i.id === item.id)
      if (existing) {
        existing.quantity += item.quantity
      } else {
        state.items.push(item)
      }
    },
    REMOVE_ITEM(state, itemId) {
      state.items = state.items.filter(i => i.id !== itemId)
    }
  },
  actions: {
    async addToCart({ commit }, product) {
      // 可以在这里调用 API
      commit('ADD_ITEM', { ...product, quantity: 1 })
    }
  }
}

重构后的组件代码:

<!-- Header.vue -->
<template>
  <header>
    <div class="cart-icon" @click="goToCart">
      <i class="icon-cart"></i>
      <span v-if="cartCount > 0" class="badge">{{ cartCount }}</span>
    </div>
  </header>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters('cart', ['totalCount'])
  }
}
</script>

通过这两次重构,团队总结出了组件化开发的核心心法

  1. 组件化是手段,不是目的:不要为了拆而拆,组件的边界应该对应业务的边界。
  2. 状态往上提,事件往下传:共享状态尽量放在共同的父组件或状态管理库中。
  3. 单向数据流是底线:不要直接修改 props,不要跨组件直接修改状态。

3. 组件通信的"七种武器"

在深入状态管理库之前,我们先搞清楚组件之间有哪些通信方式,以及它们各自的适用场景。

3.1 Props / Emit:父子组件的"官方通道"

这是 Vue/React 中最基础、最推荐的父子通信方式。

Vue 示例:

<!-- Parent.vue -->
<template>
  <div>
    <Child
      :user="currentUser"
      :theme="darkMode"
      @update:profile="handleProfileUpdate"
      @delete="handleDelete"
    />
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentUser: { id: 1, name: '张三' },
      darkMode: true
    }
  },
  methods: {
    handleProfileUpdate(newProfile) {
      this.currentUser = { ...this.currentUser, ...newProfile }
    },
    handleDelete(userId) {
      console.log('删除用户:', userId)
    }
  }
}
</script>
<!-- Child.vue -->
<template>
  <div :class="['user-card', theme ? 'dark' : 'light']">
    <h3>{{ user.name }}</h3>
    <input v-model="localProfile.bio" placeholder="个人简介" />
    <button @click="saveProfile">保存</button>
    <button @click="requestDelete">删除</button>
  </div>
</template>

<script>
export default {
  props: {
    user: { type: Object, required: true },
    theme: { type: Boolean, default: false }
  },
  data() {
    return {
      localProfile: { bio: '' }
    }
  },
  watch: {
    user: {
      immediate: true,
      handler(newVal) {
        this.localProfile = { ...newVal }
      }
    }
  },
  methods: {
    saveProfile() {
      // 通过事件通知父组件更新
      this.$emit('update:profile', this.localProfile)
    },
    requestDelete() {
      this.$emit('delete', this.user.id)
    }
  }
}
</script>

最佳实践:

  1. Props 向下传递,Events 向上传递:保持单向数据流
  2. Props 尽量只读:不要在子组件直接修改 props
  3. 事件命名要语义化update:xxx 表示更新,deletesubmit 表示动作

3.2 Event Bus:跨组件通信的"小道消息"

当两个没有直接父子关系的组件需要通信时,Event Bus(事件总线)是一种简单的方案。

实现方式:

// eventBus.js
import { createApp } from 'vue'

// 创建一个空的 Vue 实例作为事件总线
const EventBus = createApp({})

export default EventBus

使用示例:

<!-- 组件 A发送消息 -->
<template>
  <button @click="notifyUserLoggedIn">用户登录</button>
</template>

<script>
import EventBus from './eventBus'

export default {
  methods: {
    notifyUserLoggedIn() {
      // 发送事件,带上用户数据
      EventBus.$emit('user:login', {
        userId: 123,
        username: '张三',
        timestamp: Date.now()
      })
    }
  }
}
</script>
<!-- 组件 B接收消息 -->
<template>
  <div class="notification-bar">
    <span v-if="lastLoginUser">欢迎回来{{ lastLoginUser.username }}!</span>
  </div>
</template>

<script>
import EventBus from './eventBus'

export default {
  data() {
    return {
      lastLoginUser: null
    }
  },
  created() {
    // 监听登录事件
    EventBus.$on('user:login', this.handleUserLogin)
  },
  beforeUnmount() {
    // 重要:组件销毁时取消监听,防止内存泄漏
    EventBus.$off('user:login', this.handleUserLogin)
  },
  methods: {
    handleUserLogin(userData) {
      console.log('收到登录事件:', userData)
      this.lastLoginUser = userData

      // 3秒后清空提示
      setTimeout(() => {
        this.lastLoginUser = null
      }, 3000)
    }
  }
}
</script>

Event Bus 的优缺点:

优点 缺点
实现简单,无需额外依赖 难以追踪事件流向,调试困难
适合小范围、临时性的通信 容易形成"事件 spaghetti"
解耦发送方和接收方 必须手动管理订阅/取消订阅,容易内存泄漏

什么时候用,什么时候不用:

  • 可以用:两个距离较远、但逻辑上有关联的组件,且通信频率不高
  • 不要用:组件层级简单、可以用 props/emit 解决;或者通信逻辑复杂、需要状态持久化

3.3 Provide / Inject:跨层级传值的"秘密通道"

当数据需要从祖先组件传递给深层嵌套的后代组件时,逐层传递 props 会非常繁琐。Vue 的 Provide / Inject 机制可以解决这个问题。

使用场景:

  • 主题配置(深色/浅色模式)
  • 用户信息(当前登录用户)
  • 国际化配置
  • 表单控件之间的通信

代码示例:

<!-- 祖先组件App.vue -->
<template>
  <div :class="['app', theme]">
    <!-- 深层嵌套的组件树 -->
    <Layout>
      <Sidebar>
        <Menu>
          <MenuItem v-for="item in menuItems" :key="item.id" />
        </Menu>
      </Sidebar>
      <MainContent>
        <RouterView />
      </MainContent>
    </Layout>
  </div>
</template>

<script>
import { provide, ref } from 'vue'

export default {
  setup() {
    // 响应式的主题配置
    const theme = ref('light')
    const userInfo = ref({
      id: 123,
      name: '张三',
      role: 'admin'
    })

    // 切换主题的方法
    const toggleTheme = () => {
      theme.value = theme.value === 'light' ? 'dark' : 'light'
    }

    // 提供给后代组件
    provide('theme', theme)
    provide('userInfo', userInfo)
    provide('toggleTheme', toggleTheme)

    return { theme }
  }
}
</script>
<!-- 深层后代组件MenuItem.vue可能是第 5+ 层嵌套 -->
<template>
  <div :class="['menu-item', theme]">
    <span class="icon">{{ icon }}</span>
    <span class="label">{{ label }}</span>

    <!-- 管理员才能看到设置按钮 -->
    <button
      v-if="userInfo?.role === 'admin'"
      class="settings-btn"
      @click="openSettings"
    >
      设置
    </button>
  </div>
</template>

<script>
import { inject } from 'vue'

export default {
  props: {
    icon: String,
    label: String
  },
  setup() {
    // 注入祖先提供的数据
    const theme = inject('theme', 'light') // 提供默认值
    const userInfo = inject('userInfo', {})

    const openSettings = () => {
      console.log('打开设置,当前主题:', theme.value)
    }

    return { theme, userInfo, openSettings }
  }
}
</script>

Provide / Inject 的特点:

优点 缺点
解决跨层级通信问题,无需逐层传递 props 破坏了组件的封装性,难以追踪数据来源
适合提供全局配置、主题、用户上下文等 过度使用会导致组件间耦合严重
与响应式系统配合良好 不适合频繁变化的数据(如表单输入)

使用建议:

  • 适合:主题、语言、当前用户、全局配置等相对稳定的数据
  • 不适合:频繁变化的业务数据、组件间复杂的交互逻辑

4. 状态管理的"进化论"

前面我们讲了组件之间的通信方式,但当应用规模进一步扩大,单纯依靠组件自身的机制已经不够用了。这时候就需要专门的状态管理方案。

4.1 为什么需要专门的状态管理?

让我们看一个典型的购物车场景:

没有状态管理时的问题:

// 问题1:状态分散在各个组件
// Header.vue 有自己的 cartCount
// CartPage.vue 有自己的 cartItems
// ProductDetail.vue 也有自己的 localCart

// 问题2:同步困难
// 在 ProductDetail 添加商品到购物车
// Header 的购物车数量不会自动更新

// 问题3:数据不一致
// 用户在 CartPage 删除了商品
// 但 ProductDetail 的"已加入购物车"按钮还是选中状态

// 问题4:难以持久化
// 用户刷新页面,购物车数据丢失

有状态管理时的好处:

// 1. 单一数据源
// 整个应用只有一份购物车状态
const store = {
  cart: {
    items: [],
    selectedIds: []
  }
}

// 2. 响应式同步
// 任何地方修改购物车,所有相关组件自动更新

// 3. 状态可追溯
// 每次修改都有记录,可以调试、回滚

// 4. 易于持久化
// 可以方便地保存到 localStorage 或服务器

4.2 状态管理的核心概念

无论你选择哪种状态管理方案,以下概念都是通用的:

概念 解释 类比
State 应用的状态数据 数据库中的数据
Getter 从 state 派生的计算属性 SQL 查询视图
Mutation 修改 state 的唯一方式(同步) 数据库事务
Action 提交 mutation 的方法(可异步) 业务逻辑层
Store 容纳以上所有内容的对象 数据库实例

4.3 主流状态管理库对比

特性 Redux Vuex Pinia MobX Zustand Jotai
框架 React Vue Vue React/Vue React React
学习曲线 陡峭 中等 平缓 中等 平缓 平缓
样板代码 中等 极少 极少
TypeScript 良好 良好 优秀 良好 优秀 优秀
中间件支持 丰富
适用场景 大型应用 中大型 Vue 应用 中小型 Vue 应用 中大型应用 中小型应用 原子化状态

5. 各框架状态管理详解

5.1 Redux:严格而强大的状态管理

Redux 是 React 生态中最经典的状态管理方案,以严格的单向数据流著称。

核心原则:

  1. 单一数据源:整个应用的 state 存储在一个对象树中
  2. State 只读:唯一改变 state 的方法是触发 action
  3. 使用纯函数修改Reducer 必须是纯函数

代码示例:

// 1. Action Types
const ADD_TODO = 'ADD_TODO'
const TOGGLE_TODO = 'TOGGLE_TODO'

// 2. Action Creators
const addTodo = (text) => ({
  type: ADD_TODO,
  payload: { id: Date.now(), text, completed: false }
})

const toggleTodo = (id) => ({
  type: TOGGLE_TODO,
  payload: { id }
})

// 3. Reducer
const initialState = {
  todos: [],
  filter: 'all' // all, active, completed
}

const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [...state.todos, action.payload]
      }
    case TOGGLE_TODO:
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      }
    default:
      return state
  }
}

// 4. Store
import { createStore } from 'redux'
const store = createStore(todoReducer)

// 5. 在 React 中使用
import { useSelector, useDispatch } from 'react-redux'

function TodoList() {
  const todos = useSelector(state => state.todos)
  const dispatch = useDispatch()

  return (
    <ul>
      {todos.map(todo => (
        <li
          key={todo.id}
          onClick={() => dispatch(toggleTodo(todo.id))}
          style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
        >
          {todo.text}
        </li>
      ))}
    </ul>
  )
}

Redux 的优缺点:

优点 缺点
严格的数据流,易于调试 样板代码多,学习曲线陡峭
时间旅行调试(Time Travel 简单的状态也需要写很多代码
丰富的中间件生态(redux-thunk, redux-saga 不适合小型项目
可预测的状态更新 需要理解函数式编程概念

5.2 Vuex 与 PiniaVue 生态的状态管理双雄

5.2.1 Vuex:经典的选择

Vuex 是 Vue 2 时代的官方状态管理库,设计理念与 Redux 类似,但更贴合 Vue 的响应式系统。

核心概念:

// store/index.js
import { createStore } from 'vuex'

export default createStore({
  // State: 存储应用的状态数据
  state: {
    user: null,
    cart: {
      items: [],
      selectedIds: []
    },
    theme: 'light'
  },

  // Getters: 从 state 派生的计算属性
  getters: {
    isLoggedIn: state => !!state.user,
    cartTotal: state => {
      return state.cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    },
    cartItemCount: state => {
      return state.cart.items.reduce((count, item) => count + item.quantity, 0)
    }
  },

  // Mutations: 修改 state 的唯一方式(必须是同步的)
  mutations: {
    SET_USER(state, user) {
      state.user = user
    },
    ADD_TO_CART(state, product) {
      const existing = state.cart.items.find(item => item.id === product.id)
      if (existing) {
        existing.quantity++
      } else {
        state.cart.items.push({ ...product, quantity: 1 })
      }
    },
    REMOVE_FROM_CART(state, productId) {
      state.cart.items = state.cart.items.filter(item => item.id !== productId)
    },
    SET_THEME(state, theme) {
      state.theme = theme
    }
  },

  // Actions: 提交 mutation 的方法(可以包含异步操作)
  actions: {
    // 用户登录
    async login({ commit }, credentials) {
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify(credentials)
        })
        const user = await response.json()
        commit('SET_USER', user)
        return { success: true }
      } catch (error) {
        return { success: false, error: error.message }
      }
    },

    // 添加到购物车(可能涉及 API 调用)
    async addToCart({ commit, state }, product) {
      // 先乐观更新 UI
      commit('ADD_TO_CART', product)

      try {
        // 同步到服务器
        await fetch('/api/cart', {
          method: 'POST',
          body: JSON.stringify({
            productId: product.id,
            quantity: 1
          })
        })
      } catch (error) {
        // 如果失败,回滚状态
        commit('REMOVE_FROM_CART', product.id)
        throw error
      }
    }
  },

  // Modules: 将 store 分割成模块
  modules: {
    user: {
      namespaced: true,
      state: () => ({
        profile: null,
        preferences: {}
      }),
      mutations: {
        SET_PROFILE(state, profile) {
          state.profile = profile
        }
      }
    },
    cart: {
      namespaced: true,
      state: () => ({
        items: [],
        selectedIds: []
      }),
      getters: {
        total: state => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
      }
    }
  }
})

在组件中使用 Vuex

<template>
  <div class="cart">
    <span>购物车 ({{ cartItemCount }})</span>
    <span>总计: ¥{{ cartTotal }}</span>
    <button @click="addItem">添加商品</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    // 方式1:使用辅助函数
    ...mapState(['user']),
    ...mapGetters(['cartTotal', 'cartItemCount']),

    // 方式2:使用命名空间模块
    ...mapState('cart', ['items']),

    // 方式3:直接访问(适合简单场景)
    userName() {
      return this.$store.state.user?.name
    }
  },
  methods: {
    ...mapActions(['addToCart']),

    async addItem() {
      await this.addToCart({ id: 1, name: '商品A', price: 100 })
    }
  }
}
</script>

5.2.2 Pinia:新一代的优雅之选

Pinia 是 Vue 团队官方推荐的新一代状态管理库,专为 Vue 3 设计,但也支持 Vue 2。

Pinia 相比 Vuex 的优势:

特性 Vuex Pinia
语法 选项式 API 组合式 API(更现代)
类型支持 需要额外配置 原生 TypeScript 支持
代码量 需要 mutations、actions 分开 更简洁,直接修改 state
模块化 需要 namespaced 自动模块化
开发工具 支持 支持,且体验更好

Pinia 代码示例:

// stores/counter.js
import { defineStore } from 'pinia'

// 方式1:选项式 API(类似 Vuex)
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: '计数器'
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    // 可以访问其他 getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  actions: {
    increment() {
      this.count++
    },
    async fetchCount() {
      const response = await fetch('/api/count')
      const data = await response.json()
      this.count = data.count
    }
  }
})

// 方式2:组合式 API(推荐,更符合 Vue3 风格)
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // State
  const items = ref([])
  const selectedIds = ref([])

  // Getters
  const totalCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const totalPrice = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  // Actions
  function addItem(product) {
    const existing = items.value.find(item => item.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  function removeItem(productId) {
    items.value = items.value.filter(item => item.id !== productId)
  }

  async function checkout() {
    // 调用支付 API
    const response = await fetch('/api/checkout', {
      method: 'POST',
      body: JSON.stringify({ items: items.value })
    })
    if (response.ok) {
      items.value = [] // 清空购物车
    }
  }

  return {
    items,
    selectedIds,
    totalCount,
    totalPrice,
    addItem,
    removeItem,
    checkout
  }
})

在组件中使用 Pinia

<template>
  <div class="cart-page">
    <h2>购物车 ({{ cart.totalCount }})</h2>

    <div v-for="item in cart.items" :key="item.id" class="cart-item">
      <span>{{ item.name }}</span>
      <span>¥{{ item.price }} x {{ item.quantity }}</span>
      <button @click="cart.removeItem(item.id)">删除</button>
    </div>

    <div class="cart-summary">
      <p>总计: ¥{{ cart.totalPrice }}</p>
      <button @click="cart.checkout" :disabled="cart.items.length === 0">
        结算
      </button>
    </div>
  </div>
</template>

<script setup>
import { useCartStore } from '@/stores/cart'

// 直接获取 store 实例,所有属性和方法都是响应式的
const cart = useCartStore()
</script>

Vuex vs Pinia 的选择建议:

  • 新项目:直接选择 Pinia,官方推荐,体验更好
  • Vue 2 老项目:可以继续使用 Vuex,或者迁移到 PiniaPinia 支持 Vue 2
  • 大型项目需要中间件:如果依赖 Redux 生态的中间件(如 redux-saga),可以考虑 Redux

5.3 MobX:响应式编程的魔法

MobX 是一个基于响应式编程(Reactive Programming)的状态管理库,通过自动追踪依赖来实现状态更新。

核心概念:

import { makeAutoObservable } from 'mobx'

class TodoStore {
  todos = []
  filter = 'all' // all, active, completed

  constructor() {
    // 自动将所有属性和方法转为响应式
    makeAutoObservable(this)
  }

  // 计算属性(自动追踪依赖)
  get filteredTodos() {
    switch (this.filter) {
      case 'active':
        return this.todos.filter(t => !t.completed)
      case 'completed':
        return this.todos.filter(t => t.completed)
      default:
        return this.todos
    }
  }

  get stats() {
    return {
      total: this.todos.length,
      active: this.todos.filter(t => !t.completed).length,
      completed: this.todos.filter(t => t.completed).length
    }
  }

  // Action:修改状态
  addTodo(text) {
    this.todos.push({
      id: Date.now(),
      text,
      completed: false
    })
  }

  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }

  removeTodo(id) {
    this.todos = this.todos.filter(t => t.id !== id)
  }

  setFilter(filter) {
    this.filter = filter
  }
}

// 创建 store 实例
const todoStore = new TodoStore()
export default todoStore

在 React 中使用:

import { observer } from 'mobx-react-lite'
import todoStore from './TodoStore'

// 使用 observer 包裹组件,使其自动追踪使用的状态
const TodoList = observer(() => {
  return (
    <div>
      <div className="filters">
        {['all', 'active', 'completed'].map(filter => (
          <button
            key={filter}
            onClick={() => todoStore.setFilter(filter)}
            className={todoStore.filter === filter ? 'active' : ''}
          >
            {filter}
          </button>
        ))}
      </div>

      <ul>
        {todoStore.filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => todoStore.toggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => todoStore.removeTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>

      <div className="stats">
        总计: {todoStore.stats.total} |
        进行中: {todoStore.stats.active} |
        已完成: {todoStore.stats.completed}
      </div>
    </div>
  )
})

MobX 的优缺点:

优点 缺点
自动追踪依赖,无需手动优化 魔法般的响应式可能让人困惑
代码量少,接近原生 JavaScript 装饰器语法需要额外配置
面向对象风格,易于理解 调试工具不如 Redux 强大
性能优秀,只更新需要的组件 大型团队需要约定规范

5.4 Zustand 与 Jotai:轻量级的新星

5.4.1 Zustand:极简主义的选择

Zustand(德语"状态")是一个轻量级的状态管理库,核心代码只有几百行。

基本用法:

import { create } from 'zustand'

// 创建 store
const useStore = create((set, get) => ({
  // State
  bears: 0,
  fish: 100,

  // Actions
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),

  // 使用 get 访问当前状态
  eatFish: () => {
    const { fish, bears } = get()
    if (fish > 0) {
      set({ fish: fish - 1, bears: bears + 0.1 })
    }
  },

  // 异步 action
  fetchBears: async () => {
    const response = await fetch('/api/bears')
    const bears = await response.json()
    set({ bears })
  }
}))

// 在 React 组件中使用
function BearCounter() {
  // 只订阅需要的字段,实现细粒度更新
  const bears = useStore((state) => state.bears)
  const increase = useStore((state) => state.increasePopulation)

  return (
    <div>
      <h1>{bears} bears around here...</h1>
      <button onClick={increase}>Add bear</button>
    </div>
  )
}

Zustand 的优势:

  1. 极简 API:核心只有 createsetget 三个概念
  2. 无需 Provider:没有 Context 的包裹问题
  3. 细粒度订阅:组件只订阅需要的状态片段
  4. 中间件支持:持久化、日志、 immer 等
// 使用中间件
import { persist, createJSONStorage } from 'zustand/middleware'

const useStore = create(
  persist(
    (set, get) => ({
      bears: 0,
      increase: () => set((state) => ({ bears: state.bears + 1 }))
    }),
    {
      name: 'bear-storage', // localStorage 的 key
      storage: createJSONStorage(() => localStorage)
    }
  )
)

5.4.2 Jotai:原子化的状态管理

Jotai 采用"原子(Atom)"的概念来管理状态,灵感来自 Recoil。

核心概念:

import { atom, useAtom } from 'jotai'

// 定义原子状态
const countAtom = atom(0)

// 派生原子(类似 computed
const doubledCountAtom = atom((get) => get(countAtom) * 2)

// 可写派生原子
const incrementAtom = atom(
  (get) => get(countAtom),
  (get, set, update) => {
    set(countAtom, get(countAtom) + update)
  }
)

// 在组件中使用
function Counter() {
  const [count, setCount] = useAtom(countAtom)
  const [doubled] = useAtom(doubledCountAtom)
  const [, increment] = useAtom(incrementAtom)

  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={() => increment(5)}>+5</button>
    </div>
  )
}

Jotai 的特点:

  1. 原子化:状态拆分成独立的小单元,按需组合
  2. 细粒度更新:只有订阅的原子变化时才重渲染
  3. 派生状态:类似 computed,自动追踪依赖
  4. 异步支持:内置对异步原子的支持
// 异步原子
const userAtom = atom(null)

const fetchUserAtom = atom(
  (get) => get(userAtom),
  async (get, set, userId) => {
    const response = await fetch(`/api/users/${userId}`)
    const user = await response.json()
    set(userAtom, user)
  }
)

// 使用
function UserProfile({ userId }) {
  const [user, fetchUser] = useAtom(fetchUserAtom)

  useEffect(() => {
    fetchUser(userId)
  }, [userId])

  if (!user) return <div>Loading...</div>
  return <div>Hello, {user.name}</div>
}

6. 如何选择适合你的状态管理方案?

面对这么多选择,你可能会感到困惑。以下是一些实用的决策建议:

6.1 决策树

你的项目规模?
├── 小型项目 (< 10 个组件)
│   └── 使用组件自带的状态管理即可
│       (useState / ref
│
├── 中型项目 (10-50 个组件)
│   ├── 使用 Vue?
│   │   └── Pinia(推荐)或 Vuex
│   └── 使用 React?
│       ├── 需要严格的数据流?→ Redux Toolkit
│       └── 追求简洁?→ Zustand
│
└── 大型项目 (> 50 个组件,多团队协作)
    ├── 使用 Vue?
    │   ├── 需要严格规范?→ Vuex + Modules
    │   └── 追求开发效率?→ Pinia
    ├── 使用 React?
    │   ├── 企业级应用?→ Redux + Redux Toolkit
    │   ├── 需要细粒度控制?→ Recoil / Jotai
    │   └── 追求简洁?→ Zustand
    └── 跨框架需求?
        └── 考虑 RxJS 或原生事件机制

6.2 各方案适用场景速查表

如果你... 推荐方案 理由
刚开始学 Vue 3 Pinia 官方推荐,语法简单,TypeScript 支持好
维护 Vue 2 老项目 Vuex 稳定成熟,生态丰富
做企业级 React 项目 Redux Toolkit 规范严格,调试工具强大
做中小型 React 项目 Zustand 极简,无样板代码
需要原子化状态管理 Jotai / Recoil 细粒度控制,派生状态强大
喜欢面向对象风格 MobX 自动追踪依赖,代码直观
追求极致性能 原生 Context + useReducer 零依赖,完全可控

6.3 避坑指南

不要这样用:

// ❌ 错误:直接修改 state
store.state.user.name = '李四'

// ❌ 错误:在组件外修改 state
setTimeout(() => {
  store.count++
}, 1000)

// ❌ 错误:在 getter 中修改 state
getters: {
  doubleCount(state) {
    state.count *= 2  // 副作用!
    return state.count
  }
}

// ❌ 错误:不取消事件监听
export default {
  created() {
    EventBus.$on('event', this.handler)
  }
  // 忘记在 beforeUnmount 中取消订阅
}

推荐这样用:

// ✅ 正确:通过 mutation/action 修改 state
store.commit('SET_USER_NAME', '李四')
// 或 Pinia
store.userName = '李四'  // Pinia 允许直接修改,因为它底层已经处理了响应式

// ✅ 正确:在 action 中处理异步
actions: {
  async fetchUser({ commit }) {
    const user = await api.getUser()
    commit('SET_USER', user)
  }
}

// ✅ 正确:getter 是纯函数
getters: {
  doubleCount: (state) => state.count * 2
}

// ✅ 正确:及时取消订阅
export default {
  created() {
    this.unsubscribe = store.$subscribe((mutation, state) => {
      console.log(mutation, state)
    })
  },
  beforeUnmount() {
    this.unsubscribe()
  }
}

7. 实战:电商购物车状态管理设计

让我们综合运用前面的知识,设计一个电商购物车系统的状态管理方案。

7.1 需求分析

  • 商品列表页可以添加商品到购物车
  • 购物车页面可以查看、修改数量、删除商品
  • 头部导航显示购物车商品数量
  • 支持选择/取消选择商品,计算选中商品总价
  • 支持全选/取消全选
  • 数据持久化到 localStorage

7.2 状态设计(Pinia

// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // ============ State ============
  const items = ref([])
  const selectedIds = ref([])
  const isLoading = ref(false)
  const error = ref(null)

  // 从 localStorage 恢复数据
  const initFromStorage = () => {
    const stored = localStorage.getItem('cart')
    if (stored) {
      try {
        const data = JSON.parse(stored)
        items.value = data.items || []
        selectedIds.value = data.selectedIds || []
      } catch (e) {
        console.error('Failed to parse cart data:', e)
      }
    }
  }

  // 持久化到 localStorage
  const persist = () => {
    localStorage.setItem('cart', JSON.stringify({
      items: items.value,
      selectedIds: selectedIds.value
    }))
  }

  // ============ Getters ============
  const itemCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const totalPrice = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  // 选中商品相关计算
  const selectedItems = computed(() =>
    items.value.filter(item => selectedIds.value.includes(item.id))
  )

  const selectedTotalPrice = computed(() =>
    selectedItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  const isAllSelected = computed(() =>
    items.value.length > 0 && selectedIds.value.length === items.value.length
  )

  // ============ Actions ============
  const addItem = (product) => {
    const existing = items.value.find(item => item.id === product.id)
    if (existing) {
      existing.quantity += product.quantity || 1
    } else {
      items.value.push({
        ...product,
        quantity: product.quantity || 1
      })
    }
    persist()
  }

  const updateQuantity = (productId, quantity) => {
    const item = items.value.find(item => item.id === productId)
    if (item) {
      if (quantity <= 0) {
        removeItem(productId)
      } else {
        item.quantity = quantity
        persist()
      }
    }
  }

  const removeItem = (productId) => {
    items.value = items.value.filter(item => item.id !== productId)
    selectedIds.value = selectedIds.value.filter(id => id !== productId)
    persist()
  }

  const toggleSelection = (productId) => {
    const index = selectedIds.value.indexOf(productId)
    if (index > -1) {
      selectedIds.value.splice(index, 1)
    } else {
      selectedIds.value.push(productId)
    }
    persist()
  }

  const toggleSelectAll = () => {
    if (isAllSelected.value) {
      selectedIds.value = []
    } else {
      selectedIds.value = items.value.map(item => item.id)
    }
    persist()
  }

  const clearCart = () => {
    items.value = []
    selectedIds.value = []
    persist()
  }

  // 初始化
  initFromStorage()

  return {
    // State
    items,
    selectedIds,
    isLoading,
    error,
    // Getters
    itemCount,
    totalPrice,
    selectedItems,
    selectedTotalPrice,
    isAllSelected,
    // Actions
    addItem,
    updateQuantity,
    removeItem,
    toggleSelection,
    toggleSelectAll,
    clearCart
  }
})

7.3 组件中使用

<!-- CartIcon.vue -->
<template>
  <div class="cart-icon" @click="goToCart">
    <i class="icon-cart"></i>
    <span v-if="cart.itemCount > 0" class="badge">
      {{ cart.itemCount }}
    </span>
  </div>
</template>

<script setup>
import { useCartStore } from '@/stores/cart'

const cart = useCartStore()

const goToCart = () => {
  router.push('/cart')
}
</script>
<!-- ProductCard.vue -->
<template>
  <div class="product-card">
    <img :src="product.image" :alt="product.name" />
    <h3>{{ product.name }}</h3>
    <p class="price">¥{{ product.price }}</p>
    <button @click="addToCart" :disabled="isInCart">
      {{ isInCart ? '已加入' : '加入购物车' }}
    </button>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useCartStore } from '@/stores/cart'

const props = defineProps({
  product: Object
})

const cart = useCartStore()

const isInCart = computed(() =>
  cart.items.some(item => item.id === props.product.id)
)

const addToCart = () => {
  cart.addItem({
    id: props.product.id,
    name: props.product.name,
    price: props.product.price,
    image: props.product.image
  })
}
</script>

8. 名词对照表

英文术语 中文对照 解释
State 状态 应用的数据存储,是状态管理的核心。
Props 属性 父组件向子组件传递数据的方式。
Event/Action 事件/动作 组件通知父组件或触发状态变更的方式。
Store 存储/仓库 集中管理状态的对象。
Getter 获取器 从 state 派生的计算属性。
Mutation 变更 Vuex 中修改 state 的唯一方式,必须是同步的。
Reducer 归约器 Redux 中处理 state 更新的纯函数。
Dispatch 派发 触发 action 或 mutation 的操作。
Subscribe 订阅 监听状态变化的操作。
Reactive 响应式 数据变化自动触发更新的机制。
Computed 计算属性 基于其他状态派生的状态。
Module 模块 将 store 分割成多个子模块的方式。
Namespaced 命名空间 模块内部的 action/mutation 自动带命名前缀。
Hydration 注水 SSR 中将服务端状态同步到客户端的过程。
Time Travel 时间旅行 Redux DevTools 中回溯状态变化的功能。

总结

状态管理是前端开发中永恒的话题。从简单的 props 传递到复杂的全局状态管理,没有银弹,只有适合当前场景的方案。

记住这些核心原则:

  1. 从简单开始:不要过早引入复杂的状态管理库,props 和 emit 能解决大部分问题。
  2. 状态往上提:共享状态尽量放在共同的父组件或状态管理库中。
  3. 单一数据源:避免同一份数据在多个地方存储。
  4. 不可变性:修改状态时创建新对象,而不是直接修改原对象。
  5. 按需选择:根据团队技术栈和项目规模选择合适的方案。

不同场景的选择建议:

  • 小型项目props / emit + Provide / Inject
  • 中型 Vue 项目Pinia
  • 中型 React 项目Zustand 或 React Query
  • 大型项目Redux Toolkit、Vuex/Pinia + 严格规范
  • 需要细粒度控制MobX、Jotai、Recoil

希望这篇指南能帮助你在面对复杂的状态管理问题时,做出明智的选择。