Vue.js: Pinia 状态管理

最后更新:2026-10-20

Pinia 是 Vue 3 官方推荐的状态管理库——替代 Vuex。Pinia 提供了更简洁的 API、完整的 TypeScript 支持、模块化热更新、原生 DevTools 支持。Vue 官方已经将 Pinia 作为默认推荐(Vuex 不再维护)。

Pinia 让你管理跨组件共享状态(用户信息、购物车、全局设置)比 provide/inject 更结构化,比 Vuex 简洁 50%。

相关核心:computed()

1. 你将学到


2. 一个购物车"组件 5 处不一致"的噩梦

(1) 痛点:5 个组件各管各的购物车数据

Alice 的电商后台有 5 个组件需要购物车数据:

JS
// ❌ 翻车版:5 个组件 5 份数据
// CartIcon.vue
const cartCount = ref(0)

// ProductCard.vue
const localCart = ref([])

// CartPage.vue
const cart = ref({ items: [], total: 0 })

// CheckoutPage.vue
const myCart = ref([])

// Header.vue
const cartItems = ref([])

产品经理 Charlie:

"Alice,我在 ProductCard 里添加商品,购物车图标没更新!图标显示'0',但页面显示'1 item'。5 个组件 5 份购物车数据——它们不同步!"

(2) Vue Pinia 解法:1 个 store,5 个组件共享

JS
// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    total: 0
  }),
  getters: {
    itemCount: (state) => state.items.length,
    totalPrice: (state) => state.items.reduce((sum, i) => sum + i.price, 0)
  },
  actions: {
    addItem(product) {
      this.items.push(product)
      this.total += product.price
    },
    removeItem(id) {
      this.items = this.items.filter(i => i.id !== id)
    }
  }
})
VUE
<!-- CartIcon.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
// 自动响应:cart.itemCount 变了,icon 立即更新
</script>
<template>
  <span>🛒 {{ cart.itemCount }}</span>
</template>
VUE
<!-- ProductCard.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
  <button @click="cart.addItem(product)">Add to Cart</button>
</template>

1 个 store,5 个组件实时同步。

(3) 收益

使用 Pinia 后:


3. Pinia vs Vuex 5 大优势

维度 Vuex 4 (Vue 3) Pinia (Vue 3)
API 简洁 ⭐⭐⭐ 复杂(mutations / actions) ⭐⭐⭐⭐⭐ 简洁(state / getters / actions)
TypeScript ⭐⭐⭐ 需额外配置 ⭐⭐⭐⭐⭐ 原生支持
Composition API ⭐⭐ 不友好 ⭐⭐⭐⭐⭐ 一等公民
DevTools ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 改进
包大小 ~10KB ~1KB

Vue 官方推荐 Pinia,Vuex 4 进入维护模式(不再添加新特性)。


4. defineStore 两种风格

(1) Options 风格(类似 Vuex)

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

export const useCounterStore = defineStore('counter', {
  // state:数据
  state: () => ({
    count: 0,
    name: 'Counter'
  }),
  
  // getters:派生值(类似 computed)
  getters: {
    doubleCount: (state) => state.count * 2,
    isZero: (state) => state.count === 0
  },
  
  // actions:方法(类似 methods)
  actions: {
    increment() {
      this.count++
    },
    async fetchData() {
      const res = await fetch('/api/count')
      this.count = await res.json()
    }
  }
})

(2) Setup 风格(推荐,组合式 API)

JS
// stores/auth.js
import { defineStore } from 'pinia'
const { ref, computed } = Vue

export const useAuthStore = defineStore('auth', () => {
  // 1. state(用 ref)
  const user = ref(null)
  const token = ref(localStorage.getItem('token') || '')
  
  // 2. getters(用 computed)
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name || 'Guest')
  
  // 3. actions(普通函数)
  function login(credentials) {
    // API call...
    user.value = { name: 'Alice' }
    token.value = 'xxx'
  }
  
  function logout() {
    user.value = null
    token.value = ''
  }
  
  return { user, token, isLoggedIn, userName, login, logout }
})

(3) Options vs Setup 对比

维度 Options 风格 Setup 风格
写法 state: () => ({}) const x = ref()
getters (state) => ... computed()
actions function() { this.x } 普通函数
TypeScript 需手动 自动推断
组合能力 较弱 强(可用其他 composable)
推荐度 熟悉 Vuex 用 新项目推荐

▶ 示例:用 reactive 模拟 defineStore 的 Options 风格

浏览器里没有 Pinia 的 CDN 依赖,这里用 reactive + provide/inject 手写一个 store:state 就是对象字段,getters 就是 get 访问器,actions 就是方法。

▶ 试一试

输出:

TEXT
根组件面板显示「count: 0 | doubleCount: 0 | isZero: true」,子组件面板显示「count: 0 | doubleCount: 0」。
点根组件「+1」两次,两处同时变成 count: 2、doubleCount: 4,isZero 变为 false。
点子组件「子组件 +1」后根组件同步到 count: 3、doubleCount: 6;点「reset」两处一起归零,isZero 回到 true。

5. 5 大核心 API

(1) state:数据

JS
// Options 风格
state: () => ({
  count: 0,
  user: null,
  items: []
})

// Setup 风格
const count = ref(0)
const user = ref(null)
const items = ref([])

(2) getters:派生值

JS
// Options 风格
getters: {
  // 简单 getter
  doubleCount: (state) => state.count * 2,
  
  // 访问其他 getter(用 this)
  ratio(state) {
    return this.doubleCount / 100
  },
  
  // 返回函数(参数化 getter)
  getItemById: (state) => (id) => {
    return state.items.find(i => i.id === id)
  }
}

// Setup 风格
const doubleCount = computed(() => count.value * 2)
const getItemById = (id) => items.value.find(i => i.id === id)

(3) actions:方法

JS
// Options 风格
actions: {
  // 同步
  increment() {
    this.count++
  },
  
  // 异步
  async fetchData() {
    const res = await fetch('/api/data')
    this.data = await res.json()
  },
  
  // 访问其他 actions
  async loginAndFetch(credentials) {
    await this.login(credentials)
    await this.fetchUser()
  }
}

// Setup 风格
function increment() {
  count.value++
}
async function fetchData() {
  const res = await fetch('/api/data')
  data.value = await res.json()
}

(4) 组件中使用

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

const cart = useCartStore()

// 1. 直接访问 state(响应式)
console.log(cart.items)

// 2. 用 storeToRefs 解构(保持响应式)
const { items, total } = storeToRefs(cart)

// 3. 调用 action
cart.addItem(product)

// 4. 监听 state 变化
watch(() => cart.items, (newItems) => {
  console.log('Cart changed:', newItems)
})
</script>

(5) 5 大注意事项

JS
// ⚠️ 注意 1:解构 state 丢失响应式
const { items } = cart  // ❌ items 是普通值
const { items } = storeToRefs(cart)  // ✅ items 是 ref

// ⚠️ 注意 2:修改 state 必须用 action
cart.items.push(...)  // ❌ 不推荐(直接修改)
cart.addItem(...)     // ✅ 用 action

// ⚠️ 注意 3:action 内的 this 指向 store
actions: {
  increment() {
    this.count++  // ✅ this = store
  }
}

// ⚠️ 注意 4:getter 缓存
getters: {
  doubleCount() {
    console.log('recomputed')  // 只在依赖变化时打印
    return this.count * 2
  }
}

// ⚠️ 注意 5:useStore 在 setup 外使用要传 pinia 实例
import { getActivePinia } from 'pinia'
const cart = useCartStore(getActivePinia())  // 在 .js 文件中

6. Pinia 持久化

(1) 安装插件

BASH
npm install pinia-plugin-persistedstate

(2) main.js 配置

(3) 5 种持久化配置

JS
// stores/cart.js
export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  
  return { items }
}, {
  // 1. 默认:localStorage key 为 'cart'
  persist: true,
  
  // 2. 自定义 key
  persist: {
    key: 'my-cart',
    storage: localStorage
  },
  
  // 3. 只持久化部分 state
  persist: {
    paths: ['items']  // 只保存 items,不保存 total
  },
  
  // 4. sessionStorage(关闭浏览器就清除)
  persist: {
    storage: sessionStorage
  },
  
  // 5. 自定义序列化(加密等)
  persist: {
    serializer: {
      serialize: (value) => btoa(JSON.stringify(value)),
      deserialize: (value) => JSON.parse(atob(value))
    }
  }
})

▶ 示例:用 watch + localStorage 手写持久化 store

这就是 pinia-plugin-persistedstate 干的事:启动时读缓存恢复 state,state 一变就用 watch 深监听写回 localStorage。

▶ 试一试

输出:

TEXT
首次打开显示「主题:light | 购物车:0 件」,下方 localStorage 内容为 (空),子组件徽章是 0。
点「切换主题」→ 主题变 dark;点「加入一件商品」两次 → 购物车 2 件、子组件徽章同步为 2。
localStorage 内容实时变成 {"theme":"dark","items":[{"id":1766...},{"id":1766...}]}。
刷新页面:主题仍是 dark、购物车仍是 2 件(启动时从缓存恢复);点「清空购物车」后 items 变空数组。

7. 综合演练:多组件共享同一个 Store

▶ 示例:Pinia 风格全局状态(CDN 用 provide/inject 模拟)

▶ 展开运行示例(109 行)
▶ 试一试

输出:

TEXT
页面顶部说明文字下依次出现 3 个面板:计数器组件 A、计数器组件 B 和购物车。
两个计数器初始都显示「count: 0 | double: 0」;点 A 的 +1 后两处同时变为 1 / 2,点 B 的 +1 也会同步到 A。
购物车初始显示「items: 0 件, total: $0」;点「加 1 件 iPhone」变为 1 件、$999,再点变 2 件、$1998;点「清空」归零。
⚠️ 此为教学演示 Pinia 核心思想(单例 + provide/inject)。生产项目用官方 pinia 库(必须 Vite),提供 DevTools、TypeScript、模块化拆分等。

(1) 📊 5 种核心 API 速查(Pinia)

API 作用 例子
state 响应式状态 const count = ref(0)
getters 计算派生(缓存) computed(() => count * 2)
actions 方法(可异步) function increment() { count++ }
storeToRefs 解构保持响应 const { count } = storeToRefs(cart)
持久化 localStorage 同步 pinia-plugin-persistedstate

(2) 📊 5 个常见错误速查

错误 现象 解决
解构 state 丢失响应 数据不变 用 storeToRefs
直接改 state 警告 改用 action
异步 action 无 await 不等结果 await cart.fetchData()
pinia 未注册 useStore is not a function main.js app.use(pinia)
持久化后字段未持久 key 不对 检查 paths 配置

(3) 📊 5 大实战场景

场景 Store 关键字段
认证 useAuthStore user, token, isLoggedIn
购物车 useCartStore items, total, itemCount
主题 useThemeStore theme, locale, density
通知 useNotificationStore notifications, unread
数据 useDataStore list, loading, error

相关核心:computed()

❓ 常见问题

Q Pinia 和 Vuex 选哪个?
A Pinia。Vue 官方已推荐 Pinia,Vuex 4 不再维护。新项目必须用 Pinia,老 Vuex 项目可逐步迁移。
Q Pinia 替代 provide/inject 吗?
A 不替代。Pinia 是全局状态管理,provide/inject 是跨层级通信。Pinia 用于"应用全局状态"(用户/购物车),provide/inject 用于"主题/i18n/配置"。
Q 什么时候用 Pinia vs Composable?
A Pinia 用于跨组件共享的全局状态(用户、购物车)。Composable 用于组件级复用逻辑(useMouse、useFetch)。Pinia 更结构化,有 DevTools。
Q Pinia 怎么模块化?
A 每个 store 一个文件,放在 src/stores/ 目录。Pinia 自动支持 tree-shaking。
Q storeToRefs 什么时候用?
A 解构 state 时必须用,否则丢失响应式。getters 和 actions 不需要。
Q Pinia 持久化支持什么存储?
A 默认 localStorage,也支持 sessionStorage、自定义 storage(IndexedDB、Cookie 等)、自定义序列化(加密)。
Q Pinia 性能如何?
A state 修改触发响应式更新,getter 自动缓存,DevTools 优化。比 Vuex 快 30-50%(没有 mutations 概念)。
Q Pinia 在 SSR 中能用吗?
A 能。import { createPinia } from 'pinia',每个请求创建独立实例。Nuxt 3 内置 Pinia 集成。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的计数 store:

    • count state
    • doubleCount getter
    • increment / decrement / reset actions
    • 在 3 个组件中共享
  2. 进阶题(难度⭐⭐) 实现购物车 store:

    • items array
    • itemCount / totalPrice getters
    • addItem / removeItem / clearCart actions
    • 持久化到 localStorage
    • 在 5 个组件中测试(ProductCard / CartIcon / CartPage / Checkout / Header)
  3. 挑战题(难度⭐⭐⭐) 实现完整的电商后台 store 系统:

    1. 5 个 store:auth / cart / theme / notification / product
    2. 每个 store 完整 state + getters + actions
    3. 持久化(auth/cart/theme)
    4. Setup 风格 + TypeScript
    5. storeToRefs 解构
    6. 跨 store 调用(cart 调用 auth 检查登录)
    7. 单元测试(用 Vitest)
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏