Skip to content

2-3 Pinia 状态管理

组件之间传数据,前面学的是 props(父→子)/ emit(子→父)。但如果是两个隔很远的组件要共享数据(比如「登录用户信息」「购物车」全站都要用),一层层传太痛苦。这时用 Pinia——全局状态管理。

配套代码:vue-demo/src/stores/counter.jsviews/CounterView.vue。Pinia 是 Vue 官方推荐的状态管理库(取代旧的 Vuex)。


1. 为什么需要它

设想:顶部导航要显示购物车数量,商品页要往购物车加东西,结算页要读购物车……这三个组件位置很远。用 props/emit 传递会非常绕。

Pinia 提供一个「全局仓库(store)」,任何组件都能直接读写,数据还是响应式的。

后端类比:Pinia store ≈ 一个全局单例 Service / 内存缓存。任何地方注入它、读它的字段、调它的方法。数据变了,所有用到的组件界面自动更新。


2. 定义一个 store

src/stores/counter.js:

js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 'counter' 是这个 store 的唯一 id
export const useCounterStore = defineStore('counter', () => {
  // state:数据(用 ref) —— 相当于 Service 的成员变量
  const count = ref(0)

  // getters:派生数据(用 computed) —— 相当于 getter 方法
  const double = computed(() => count.value * 2)

  // actions:方法 —— 相当于 Service 的业务方法
  function increment() { count.value++ }
  function reset() { count.value = 0 }

  // 把要暴露的都 return 出去
  return { count, double, increment, reset }
})

这种写法叫「setup 风格」,和写组件的 setup 几乎一样:ref 是状态,computed 是 getter,函数是 action。很好记


3. 在组件里使用 store

vue
<script setup>
import { useCounterStore } from '../stores/counter'

const counter = useCounterStore()   // 拿到全局仓库实例
</script>

<template>
  <p>count:{{ counter.count }}</p>
  <p>double:{{ counter.double }}</p>
  <button @click="counter.increment()">+1</button>
  <button @click="counter.reset()">重置</button>
</template>

直接 counter.count 读、counter.increment() 改。任何组件这样拿到的都是同一个仓库——A 组件改了 count,B 组件看到的也变了。这就是「全局共享」。


4. 解构的坑(重要)

想少写 counter.,直接解构会丢失响应式:

js
// ❌ 错误:这样解构出来的 count 不再响应式,界面不会更新
const { count, double } = useCounterStore()

要用 storeToRefs 保持响应式:

js
import { storeToRefs } from 'pinia'

const counter = useCounterStore()
// ✅ state 和 getters 用 storeToRefs 解构
const { count, double } = storeToRefs(counter)
// ✅ action(方法)可以直接解构,不受影响
const { increment, reset } = counter

记忆:数据(state/getter)用 storeToRefs 解构;方法直接解构。


5. 一个更实际的例子:购物车 store

js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])   // [{ id, name, price, qty }]

  const totalCount = computed(() =>
    items.value.reduce((s, i) => s + i.qty, 0))
  const totalPrice = computed(() =>
    items.value.reduce((s, i) => s + i.price * i.qty, 0))

  function add(product) {
    const found = items.value.find(i => i.id === product.id)
    if (found) found.qty++
    else items.value.push({ ...product, qty: 1 })
  }
  function remove(id) {
    items.value = items.value.filter(i => i.id !== id)
  }

  return { items, totalCount, totalPrice, add, remove }
})

之后:导航组件用 totalCount 显示角标,商品页调 add(),购物车页读 items、调 remove()。互不直接通信,全靠这个 store。第 5 部分实战就是这么干的。


6. 数据持久化(进阶,先了解)

刷新页面后 store 数据会清空(在内存里)。要保留(如登录态),可在 action 里同步写 localStorage,或用插件 pinia-plugin-persistedstate。第 5 部分会演示登录态持久化。


动手练习(用 vue-demo)

  1. 跑项目,进「计数器」页,点 +1,看 count 和 double 变化。
  2. 关键体验:看 App.vue 导航栏里也显示了 count(用了同一个 store)。在计数器页点 +1,导航栏的数字同步变——这就是全局共享。
  3. 打开 stores/counter.js,加一个 action decrement,在页面加个 −1 按钮调用它。
  4. 试试错误解构:在某组件 const { count } = useCounterStore(),你会发现点按钮 count 不更新;改用 storeToRefs 修好它。

小结

  • Pinia = 全局响应式仓库,解决跨组件共享数据,像全局单例 Service。
  • defineStore('id', () => { ... }):ref 是 state,computed 是 getter,函数是 action。
  • 组件里 useXxxStore() 拿实例,直接读写。
  • 解构数据要用 storeToRefs,方法可直接解构。

➡️ 下一章:2-4 axios 调后端接口

写给有后端基础、想系统学习 Vue 3 与 H5 的开发者