1.1 为什么需要状态管理
在一个 Vue 应用里,组件之间靠 props 向下传、emit 向上抛来通信。但当"用户登录信息""购物车""主题设置"这类被很多不相关组件共享的状态出现时,逐层传递会变成噩梦(俗称 prop drilling)。
状态管理库把这类共享状态抽到组件树之外的一个集中仓库(store)里,任意组件都能直接读写,且变化是响应式的、可追踪的、可调试的。
- State(状态)
- store 里存储的响应式数据本身,例如
count、user、cartItems。相当于组件的data,但全局共享。 - Getters(取值器)
- 基于 state 派生出来的计算值,带缓存,相当于 store 的
computed,例如cartTotal。 - Actions(动作)
- 修改 state 的方法,可同步可异步(发请求、改多个字段),相当于 store 的
methods。 - Store(仓库)
- state + getters + actions 的容器。Pinia 中一个应用可以有多个独立 store。
- Mutations(变更)
- Vuex 特有概念:唯一能同步修改 state 的函数,必须通过
commit调用。Pinia 已彻底移除它。 - Reactive(响应式)
- 数据变化时,依赖它的组件自动重新渲染。Pinia 基于 Vue 3 的
reactive/ref实现。
1.2 从 Vuex 到 Pinia 的演进
Vue 的官方状态管理经历了三代:
- Vuex 3(Vue 2 时代):state / getters / mutations / actions / modules 五件套,模板代码多、TS 支持弱。
- Vuex 4(兼容 Vue 3):只是让 Vuex 3 能跑在 Vue 3 上,架构没变,TS 依旧难受。
- Pinia:由 Vue 核心团队重新设计,2023 年起被官方文档标注为推荐方案,Vuex 进入维护模式不再新增特性。
Pinia 的作者 Eduardo San Martin Morote 同时是 Vue Router 的维护者。Pinia 现在就是 “Vuex 5” 的事实实现——Vue 官方文档已把它列为默认状态管理推荐。
1.3 Pinia vs Vuex 对比
| 维度 | Pinia | Vuex 4 |
|---|---|---|
| 核心概念 | state / getters / actions(3 个) | state / getters / mutations / actions(4 个) |
| 修改 state | 直接改 或 action 中改 | 必须 commit mutation |
| 模块化 | 天然扁平,多个 store 独立 | 嵌套 modules + namespaced |
| TypeScript | 自动推断,无需辅助类型 | 需大量泛型、体验差 |
| 体积(gzip) | ~1.5KB | ~3KB+ |
| 写法风格 | 选项式 + 组合式 | 仅选项式 |
| DevTools | 深度集成、时间旅行 | 支持但较弱 |
| SSR | 一等支持 | 需手动处理 |
Pinia = Vuex 去掉 mutations、去掉嵌套模块、加上完美 TS 推断和组合式写法。更少的概念,更少的模板代码。
1.4 安装
# 使用 npm / pnpm / yarn 任选其一
$ npm i pinia
$ pnpm add pinia
$ yarn add pinia
如果你用 Vue CLI 老项目,还需要 @vue/composition-api(Vue 2.7 以下);Vue 3 或 Vue 2.7+ 直接安装即可。
1.5 创建并挂载 Pinia 实例
Pinia 本身是一个"根实例",通过 createPinia() 创建,再用 app.use() 挂到 Vue 应用上。
// src/main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia); // 注册 Pinia,之后任意 store 才可用
app.mount("#app");
1.6 第一个 Store
用 defineStore 定义一个计数器 store,第一个参数是唯一 id(DevTools 中显示、SSR 序列化用)。
// src/stores/counter.ts
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({ count: 0 }),
getters: {
doubled: (state) => state.count * 2,
},
actions: {
increment() {
this.count++;
},
},
});
1.7 在组件中使用
<!-- Counter.vue -->
<script setup lang="ts">
import { useCounterStore } from "@/stores/counter";
const counter = useCounterStore();
</script>
<template>
<p>count: {{ counter.count }}</p>
<p>doubled: {{ counter.doubled }}</p>
<button @click="counter.increment()">+1</button>
</template>
store 的组合函数一律以 use 开头(如 useCounterStore),这是 Pinia 官方约定,也符合 Vue 组合式函数的习惯。
1.8 Pinia 的工作原理速览
为什么必须先 app.use(pinia) 再用 store?因为 createPinia() 创建的实例会被"注入"到当前 Vue 应用中。当你调用 useCounterStore() 时,它会从当前活跃的 Pinia 实例里取(或懒创建)对应 store:
- store 是懒加载的——首次调用
useXxxStore()才真正实例化。 - 同一个 store 在一个应用里是单例——多个组件调用拿到的是同一份。
- SSR 中每个请求会有独立的 Pinia 实例,避免状态串台(第 9 章详解)。
1.9 小结与下一步
- 状态管理解决"跨组件共享状态"的问题,Pinia 是 Vue 官方推荐方案。
- Pinia 相比 Vuex:去掉 mutations、扁平模块化、TS 自动推断、体积更小。
- 三步上手:
createPinia()→app.use(pinia)→defineStore。 - 下一章深入 defineStore——对比选项式与组合式两种写法,并教你如何选择。