定义 store:state、getters、actions
Pinia 状态管理 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
Pinia 是 Vue 官方推荐的状态管理库,替代 Vuex。这一节掌握 setup 语法定义 store、组件中读写状态、getters 与 actions 的分工,以及为什么「store 就是一个可共享的 composable」。
问题场景
购物车数据要在商品列表、头部角标、结算页三处读写。用 props 逐层传太繁琐,用 provide/inject 又缺乏集中管理。你手动写了一个 reactive({ items: [] }) 单例模块,能跑——但刷新丢数据、没有 devtools 支持、多个模块之间的依赖关系没人管得清。Pinia 就是这块的官方答案。
核心解释
先创建一个 store。Pinia 支持两种写法,setup 语法与组合式 API 一致,推荐:
// stores/cart.js
import { computed, ref } from "vue";
import { defineStore } from "pinia";
export const useCartStore = defineStore("cart", () => {
// state
const items = ref([]);
// getters
const count = computed(() => items.value.reduce((s, it) => s + it.qty, 0));
const total = computed(() =>
items.value.reduce((s, it) => s + it.price * it.qty, 0)
);
// actions
function add(product) {
const found = items.value.find((it) => it.id === product.id);
if (found) found.qty += 1;
else items.value.push({ ...product, qty: 1 });
}
function remove(id) {
items.value = items.value.filter((it) => it.id !== id);
}
return { items, count, total, add, remove };
});
组件里 const cart = useCartStore(),然后像访问普通对象一样用:cart.items、cart.count、cart.add(product)。注意不需要 .value:store 实例会把 ref 自动解包(Pinia 的响应式代理),这也是它比手写单例舒服的地方。
安装:app.use(createPinia()),必须在 app.use(router) 之前(守卫里可能用到 store)。
每个 store 用唯一的 id 注册(defineStore("cart", ...)),同一 id 在任何组件里拿到的都是同一个实例——这是「集中式状态」的实现基础。
几个关键认知:
- store 是懒创建的:第一次
useCartStore()时才实例化,之后所有组件拿到同一个实例。这保证了「同一状态源」。 - 解构会丢响应式:
const { count } = useCartStore()得到的是快照。要解构用storeToRefs(cart)。这是 Vue 响应式规则的延续(V03)。 - actions 可以是异步的:
async function checkout() { ... await fetch(...) },组件里await cart.checkout()。 - getters 就是 computed:不要用 action 去维护派生值,和 V03 的规则一致。
和 React 对照:Pinia store ≈ 一个模块级的 Zustand store(create + hooks)。区别是 Pinia 与 Vue 的响应式系统深度集成:store 里的 ref 自动解包、computed 自动追踪、devtools 能时间旅行。React 里需要 Context + useSyncExternalStore 才能达到类似效果。
示例拆解
一个带「加购、改数量、清空」的购物车,观察响应式在组件间的同步:
<!-- components/ProductList.vue -->
<script setup>
import { useCartStore } from "../stores/cart";
const cart = useCartStore();
const products = [
{ id: 1, name: "键盘", price: 399 },
{ id: 2, name: "鼠标", price: 129 },
];
</script>
<template>
<ul>
<li v-for="p in products" :key="p.id">
{{ p.name }} · {{ p.price }} 元
<button @click="cart.add(p)">加入购物车</button>
</li>
</ul>
</template>
<!-- components/CartBadge.vue -->
<script setup>
import { useCartStore } from "../stores/cart";
const cart = useCartStore();
</script>
<template>
<span class="badge">购物车 {{ cart.count }} 件 / {{ cart.total }} 元</span>
</template>
点「加入购物车」,角标立即更新——两个组件共享同一个 store 实例,响应式自动同步。打开 Vue Devtools 的 Pinia 面板,能看到 items、count、total 的实时值,还能手动修改状态调试。
动手任务
- 完成上面的购物车,验证两个组件同步,并在 Devtools 里修改
items观察界面变化。 - 用
storeToRefs解构count和total到组件里,验证解构后仍然响应式。 - 给 store 加一个
checkoutaction:模拟异步请求,期间设置loading状态,完成后清空购物车。
常见错误
- 解构 store 丢响应式:
const { count } = cart后 count 不更新。用storeToRefs。 - 在 store 定义外创建单例:
useCartStore()必须在组件 setup 或 store 内部调用,模块顶层调用会脱离 Pinia 上下文。 - 用 action 维护派生值:
watch(items, () => total.value = ...)应是 computed getter。 - 忘记
app.use(createPinia()):组件里调用 store 报「getActivePinia was called with no active Pinia」。且在 router 之前安装。
检查与复盘
- 能用 setup 语法写出含 state/getters/actions 的 store
- 能解释 store 实例为什么在组件间共享
- 能说出解构 store 的正确方式
参考资料
- Pinia 官方文档「开始」:安装与第一个 store
- Pinia 官方文档「核心概念」:state/getters/actions 的完整规则
- Vue 官方文档「状态管理」:何时引入状态库
读完这一课了?
标记完成后,工作台会安排接下来的学习。