第110课:Pinia 状态管理(上)——Store 定义、state、getters、actions
在 Vue 2 时代,全局状态管理的标准方案是 Vuex。它提供了中心化的 Store、严格的状态变更流程(通过 mutations)和模块化机制。然而 Vuex 在类型推断(TypeScript 支持)、模块嵌套的复杂性和样板代码方面存在不足。Pinia 是 Vue 官方新一代状态管理库,为 Vue 3 设计,同时也支持 Vue 2。它的核心理念是:Store 就是一个普通的 JavaScript 对象,拥有 state(数据)、getters(计算属性)和 actions(方法),完全摒弃了 mutations 这一中间层。Pinia 提供了完整的 TypeScript 类型推断、无需命名空间的多 Store 架构、以及内置的 DevTools 支持和插件生态。本节课将从零开始,使用组合式 Store(Setup Store)语法定义 Pinia Store,深入 state、getters 和 actions 的用法,并通过一个购物车实战案例串联所有概念。
1. 安装 Pinia 与创建第一个 Store
1.1 安装与挂载
1 | npm install pinia |
在 Vue 应用入口文件中创建 Pinia 实例并挂载:
1 | // main.js |
createPinia() 返回一个 Pinia 实例。app.use(pinia) 将其注册为 Vue 插件,此后所有组件都可以通过 useStore() 访问定义的 Store。
1.2 定义 Store:两种语法
Pinia 支持两种定义 Store 的语法:
- Options Store(选项式):与 Vuex 类似,使用
state、getters、actions配置对象。 - Setup Store(组合式):使用 Vue 的 Composition API,
ref即state,computed即getters,function即actions。这是推荐方式,与<script setup>心智模型完全一致。
本节课使用 Setup Store 语法。下面是第一个 Store:
1 | // stores/counter.js |
关键解析:
defineStore的第一个参数是 Store 的唯一名称(ID),用于在 DevTools 中标识和插件系统中引用。必须全局唯一。- 第二个参数是一个回调函数(
setup函数),它遵循与 Vue 组件的<script setup>完全相同的规则——ref是响应式状态,computed是计算属性,普通函数是 actions。 - 回调函数必须返回一个包含所有需要暴露的状态、计算属性和方法的对象。未在返回对象中的变量是 Store 内部的“私有”变量(外部无法访问)。
1.3 在组件中使用 Store
1 | <script setup> |
关键行为:
- Pinia Store 返回的是一个用
reactive包裹的对象。因此ref类型的 state 会自动解包,在组件中无需.value访问(counter.count即可)。 - Store 实例在组件多次调用
useCounterStore()时返回同一个对象(单例),状态在所有使用该 Store 的组件间共享。 - 可以直接修改
counter.count = 5——Pinia 允许直接修改 state(内部通过patch机制追踪变更),而无需像 Vuex 那样必须通过 mutation 修改。这极大地简化了代码,同时 DevTools 仍能追踪每次变更。
2. state:响应式数据的核心
在 Setup Store 中,state 就是通过 ref() 或 reactive() 创建的响应式变量。你需要在返回对象中暴露它们。
2.1 ref vs reactive 的选择
| 数据类型 | 推荐方式 | 说明 |
|---|---|---|
| 基本类型(数字、字符串、布尔) | ref |
ref 包装基本类型,在 Store 返回对象中自动解包。 |
| 数组 | ref |
ref([]) 可以整体替换数组引用,符合不可变更新模式。 |
| 复杂对象 | ref 或 reactive |
reactive 适用于嵌套对象,但无法整体替换;ref 更灵活(可整体替换)。 |
1 | // stores/user.js |
2.2 重置 State
Pinia 提供了 $reset() 方法,将 Store 的所有 state 重置为初始值。**仅 Options Store 支持 $reset()**。Setup Store 需要自己实现重置逻辑:
1 | const initialCount = 0; |
2.3 直接修改 State 与 $patch
你可以直接修改 Store 中的 state:counter.count = 10。Pinia 内部会通过 patch 追踪这一变更。如果需要一次性修改多个 state 属性,使用 $patch 方法可以将它们合并到一次变更中(在 DevTools 中显示为一次操作):
1 | const store = useCounterStore(); |
3. getters:Store 的计算属性
在 Setup Store 中,getters 就是使用 Vue 的 computed() 定义的计算属性。它们依赖 state 或其他 getters,并在依赖变化时自动重新计算。
3.1 基本用法
1 | // stores/cart.js |
3.2 在 getter 中访问其他 Store 的 getter
1 | // stores/order.js |
注意:在 getter(或 action)中访问其他 Store 时,直接在 setup 函数内部调用该 Store 的 useStore() 即可。由于这些代码在 Store 的 setup 函数中执行,它们遵循与组件相同的规则——可以自由使用其他 Store。
3.3 向 getter 传递参数
computed 本身不接受参数,但可以通过返回一个函数实现“带参数的 getter”:
1 | const itemById = computed(() => { |
这种方式返回的是一个普通函数(不是响应式的 getter),但因为它依赖了 items.value,当 items 变化时,返回的函数会被重新创建。
4. actions:业务逻辑与异步操作
在 Setup Store 中,actions 就是普通的 JavaScript 函数(可以是同步或异步)。你可以直接在 action 中修改 state、调用其他 actions、或使用其他 Store。
4.1 同步 Action
1 | function addItem(product) { |
4.2 异步 Action
异步 action 直接使用 async/await,无需任何额外配置(不像 Vuex 需要使用 actions + mutations 的组合来处理异步):
1 | import { ref } from 'vue'; |
与 Vuex 的对比:
- Vuex 中异步操作必须放在
actions中,通过commit('mutation')修改 state。同步修改必须在mutations中。 - Pinia 中
actions可以直接修改 state,无需mutations。同步和异步操作统一在actions(或直接在组件中)处理。这消除了 Vuex 中actions和mutations之间繁琐的类型映射和命名重复。
4.3 在 Action 中调用另一个 Action
1 | async function checkout() { |
5. 组合多个 Store:购物车完整实战
以下示例展示了 useProductStore 和 useCartStore 如何在组件中配合使用。
1 | // stores/products.js |
1 | // stores/cart.js |
1 | <!-- ShoppingView.vue --> |
设计要点:
productsStore 是商品的“数据源”,保持简单——只有 state,无 getters 和 actions。cartStore 封装了购物车的所有逻辑:addItem、removeItem、updateQuantity、clearCart,以及三个派生值totalItems、totalPrice、isEmpty。- 组件中同时使用两个 Store,各司其职,互不干扰。
useCartStore在addItem中接收来自useProductStore的商品对象,两者通过函数参数解耦。
课后练习
一、概念自测(选择题 / 填空题)
(单选) Pinia 中 Setup Store 的 state 是通过什么定义的?
A.data()函数返回的对象。
B.ref或reactive。
C.computed。
D.state()配置函数。(单选) Pinia 与 Vuex 的一个重要区别是?
A. Pinia 必须使用 mutations 修改 state。
B. Pinia 不允许在 actions 中直接修改 state。
C. Pinia 移除了 mutations,允许在 actions 中直接修改 state。
D. Pinia 不提供 DevTools 支持。(填空) 在 Setup Store 中,getters 使用 Vue 的
______函数定义。(多选) 关于 Pinia Store 的描述,哪些是正确的?
A. Store 的名称(ID)在应用中必须唯一。
B. 组件中访问 Store 的 state 时,ref 会自动解包(无需.value)。
C. 多个组件调用同一个useStore()会返回不同的 Store 实例。
D. 可以在 action 中直接调用其他 Store 的 action。
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要使用 Pinia 实现一个图书管理状态模块。要求如下:
- 使用 Setup Store 语法,Store ID 为
books。 state:books数组(初始包含 3 本静态图书{ id, title, author, available })、searchQuery字符串。getters:filteredBooks(根据searchQuery按书名模糊筛选,大小写不敏感)、availableBooks(仅返回available === true的书)、bookCount(总数量)。actions:addBook(book)(向数组追加)、removeBook(id)(按 ID 过滤)、toggleAvailability(id)(翻转available字段)、setSearchQuery(query)。- 所有函数使用 TypeScript 编写,添加 JSDoc 注释。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成完整的 Pinia Store 代码。提示词中需明确指定 state 的结构、getters 的筛选逻辑和 actions 的签名。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请使用 Pinia Setup Store 语法创建一个图书管理 Store 模块。需要创建文件
stores/books.ts:
- 使用 TypeScript。
- 定义
Book接口:{ id: number; title: string; author: string; available: boolean }。- Store ID 为
'books'。- State:
books用ref<Book[]>初始化 3 本静态数据;searchQuery用ref('')。- Getters(使用
computed):
filteredBooks:若searchQuery.value为空返回全部,否则返回books.value.filter(b => b.title.toLowerCase().includes(searchQuery.value.toLowerCase()))。availableBooks:返回filteredBooks.value中available === true的项。bookCount:返回filteredBooks.value.length。- Actions:
addBook(book: Omit<Book, 'id'>):生成id(Date.now()或books.value.length + 1),追加到books。removeBook(id: number):books.value = books.value.filter(b => b.id !== id)。toggleAvailability(id: number):查找对应 book,book.available = !book.available。setSearchQuery(query: string):searchQuery.value = query。- 导出
useBooksStore。- 添加 JSDoc 注释,确保类型安全。完成后输出完整文件内容。
四、面试真题与参考答案
题目(京东前端面试题):
请对比 Pinia 和 Vuex 的核心差异。为什么 Pinia 可以移除
mutations而不会导致状态追踪混乱?Pinia 的 Setup Store 语法相比 Options Store 有哪些优势?在什么场景下你会选择 Pinia 而不是provide/inject?
参考答案:
Pinia 与 Vuex 的核心差异:
- 移除 mutations:Vuex 强制通过
mutations修改 state,异步操作放在actions中,再通过commit调用 mutation。Pinia 允许在actions和组件中直接修改 state,内部通过$patch和 Proxy 追踪每次变更,DevTools 仍能记录完整的状态快照和时间线。这消除了 Vuex 中actions和mutations之间命名重复、类型映射繁琐的问题。 - 类型推断:Pinia 为 TypeScript 设计,Setup Store 中的
ref和computed自动推导类型,无需额外类型标注。Vuex 的 TypeScript 支持较弱,需要大量手动类型声明。 - 无需命名空间:Vuex 的模块通过
namespaced: true隔离,访问时需要store.getters['module/getter']的字符串路径。Pinia 的每个 Store 是独立的,直接通过useXxxStore()导入使用,天然的 JavaScript 模块隔离。 - 体积更小:Pinia 约 1KB(压缩后),Vuex 约 4KB。
Pinia 能移除 mutations 的原因:Vuex 引入 mutations 的初衷是通过同步函数确保状态变更是可追踪的(DevTools 能记录每次 mutation)。Pinia 内部使用 reactive 包装 Store 实例,通过 $patch 将多次修改合并为一个变更事件。开发者可以直接修改 state,Pinia 将其包装为 patch 并发送给 DevTools,因此状态追踪并未丢失。
Setup Store 的优势:与 Vue 3 的 <script setup> 心智模型完全一致——ref 即 state,computed 即 getters,function 即 actions。开发者无需学习新的 API,可以直接复用 Vue 3 组合式 API 的全部知识。同时,可以将公共逻辑提取为 composable 函数在多个 Store 间共享。
选择 Pinia 而非 provide/inject 的场景:当数据需要在多个不相关的组件树之间共享(而非仅一个组件子树内)、需要持久化或插件扩展(如 pinia-plugin-persistedstate)、需要DevTools 调试、或需要复杂的异步操作和缓存时,Pinia 是更好的选择。provide/inject 适合组件树局部范围内的轻量共享,生命周期跟随提供者组件。
课后练习答案
一、概念自测答案
B
- 解析:Setup Store 使用
ref或reactive定义 state。A 是选项式 API 的data,C 是 getters,D 是 Options Store 的state函数。
- 解析:Setup Store 使用
C
- 解析:Pinia 移除了
mutations,允许在actions中直接修改 state。A 和 B 描述的是 Vuex,D 错误(Pinia 内置 DevTools 支持)。
- 解析:Pinia 移除了
computed- 解析:Setup Store 使用 Vue 的
computed函数定义 getters。
- 解析:Setup Store 使用 Vue 的
A、B、D
- 解析:C 错误,
useStore()在同一组件或不同组件中返回同一个单例实例。A、B、D 均为正确描述。
- 解析:C 错误,
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请使用 Pinia 的 Setup Store 语法创建一个图书管理 Store。要求:
- TypeScript,Book 接口:
{ id: number; title: string; author: string; available: boolean }。- Store ID:
'books'。- state:
books(ref, 初始 3 本书)、searchQuery(ref, 空字符串)。- getters:
filteredBooks(按书名模糊筛选)、availableBooks(仅可借阅)、bookCount(总数)。- actions:
addBook,removeBook(id),toggleAvailability(id),setSearchQuery(query)。- 添加 JSDoc,导出
useBooksStore。输出完整文件。”