2-3 Pinia 状态管理
组件之间传数据,前面学的是 props(父→子)/ emit(子→父)。但如果是两个隔很远的组件要共享数据(比如「登录用户信息」「购物车」全站都要用),一层层传太痛苦。这时用 Pinia——全局状态管理。
配套代码:
vue-demo/src/stores/counter.js和views/CounterView.vue。Pinia 是 Vue 官方推荐的状态管理库(取代旧的 Vuex)。
1. 为什么需要它
设想:顶部导航要显示购物车数量,商品页要往购物车加东西,结算页要读购物车……这三个组件位置很远。用 props/emit 传递会非常绕。
Pinia 提供一个「全局仓库(store)」,任何组件都能直接读写,数据还是响应式的。
后端类比:Pinia store ≈ 一个全局单例 Service / 内存缓存。任何地方注入它、读它的字段、调它的方法。数据变了,所有用到的组件界面自动更新。
2. 定义一个 store
src/stores/counter.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
<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.,直接解构会丢失响应式:
// ❌ 错误:这样解构出来的 count 不再响应式,界面不会更新
const { count, double } = useCounterStore()要用 storeToRefs 保持响应式:
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
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,看 count 和 double 变化。
- 关键体验:看
App.vue导航栏里也显示了 count(用了同一个 store)。在计数器页点 +1,导航栏的数字同步变——这就是全局共享。 - 打开
stores/counter.js,加一个 actiondecrement,在页面加个 −1 按钮调用它。 - 试试错误解构:在某组件
const { count } = useCounterStore(),你会发现点按钮 count 不更新;改用storeToRefs修好它。
小结
- Pinia = 全局响应式仓库,解决跨组件共享数据,像全局单例 Service。
defineStore('id', () => { ... }):ref是 state,computed是 getter,函数是 action。- 组件里
useXxxStore()拿实例,直接读写。 - 解构数据要用
storeToRefs,方法可直接解构。
➡️ 下一章:2-4 axios 调后端接口