WinddSnow

Pinia-State-Management-Part1-Store-Definition-State-Getters-Actions

字数统计: 4.5k阅读时长: 20 min
2026/08/01

第110课:Pinia 状态管理(上)——Store 定义、stategettersactions

在 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,深入 stategettersactions 的用法,并通过一个购物车实战案例串联所有概念。


1. 安装 Pinia 与创建第一个 Store

1.1 安装与挂载

1
npm install pinia

在 Vue 应用入口文件中创建 Pinia 实例并挂载:

1
2
3
4
5
6
7
8
9
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');

createPinia() 返回一个 Pinia 实例。app.use(pinia) 将其注册为 Vue 插件,此后所有组件都可以通过 useStore() 访问定义的 Store。

1.2 定义 Store:两种语法

Pinia 支持两种定义 Store 的语法:

  • Options Store(选项式):与 Vuex 类似,使用 stategettersactions 配置对象。
  • Setup Store(组合式):使用 Vue 的 Composition API,refstatecomputedgettersfunctionactions。这是推荐方式,与 <script setup> 心智模型完全一致。

本节课使用 Setup Store 语法。下面是第一个 Store:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// stores/counter.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCounterStore = defineStore('counter', () => {
// state:使用 ref 定义
const count = ref(0);

// getters:使用 computed 定义
const doubleCount = computed(() => count.value * 2);
const isPositive = computed(() => count.value > 0);

// actions:使用函数定义
function increment() {
count.value++;
}

function decrement() {
count.value--;
}

function reset() {
count.value = 0;
}

// 返回需要暴露的所有属性和方法
return { count, doubleCount, isPositive, increment, decrement, reset };
});

关键解析

  • defineStore 的第一个参数是 Store 的唯一名称(ID),用于在 DevTools 中标识和插件系统中引用。必须全局唯一。
  • 第二个参数是一个回调函数(setup 函数),它遵循与 Vue 组件的 <script setup> 完全相同的规则——ref 是响应式状态,computed 是计算属性,普通函数是 actions。
  • 回调函数必须返回一个包含所有需要暴露的状态、计算属性和方法的对象。未在返回对象中的变量是 Store 内部的“私有”变量(外部无法访问)。

1.3 在组件中使用 Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { useCounterStore } from '@/stores/counter';

const counter = useCounterStore();
// 直接访问 state 和 getters(无需 .value,Pinia 自动解包 ref)
// counter.count、counter.doubleCount 可直接使用
</script>

<template>
<div>
<p>计数:{{ counter.count }}</p>
<p>双倍:{{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
<button @click="counter.decrement">-1</button>
</div>
</template>

关键行为

  • 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([]) 可以整体替换数组引用,符合不可变更新模式。
复杂对象 refreactive reactive 适用于嵌套对象,但无法整体替换;ref 更灵活(可整体替换)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// stores/user.js
import { defineStore } from 'pinia';
import { ref, reactive } from 'vue';

export const useUserStore = defineStore('user', () => {
// 使用 ref 定义基本类型和可替换的对象
const token = ref(localStorage.getItem('token') || null);
const profile = ref(null); // 可整体替换为新的用户对象

// 使用 reactive 定义固定结构的复杂对象(不推荐整体替换)
const preferences = reactive({
theme: 'light',
language: 'zh-CN',
notifications: true,
});

return { token, profile, preferences };
});

2.2 重置 State

Pinia 提供了 $reset() 方法,将 Store 的所有 state 重置为初始值。**仅 Options Store 支持 $reset()**。Setup Store 需要自己实现重置逻辑:

1
2
3
4
5
6
7
8
9
const initialCount = 0;
const count = ref(initialCount);

function $reset() {
count.value = initialCount;
// 如果还有其他 state,也逐一重置
}

return { count, $reset };

2.3 直接修改 State 与 $patch

你可以直接修改 Store 中的 state:counter.count = 10。Pinia 内部会通过 patch 追踪这一变更。如果需要一次性修改多个 state 属性,使用 $patch 方法可以将它们合并到一次变更中(在 DevTools 中显示为一次操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = useCounterStore();

// 对象形式
store.$patch({
count: 100,
// 不能修改 getters 和 actions
});

// 函数形式:适合复杂逻辑
store.$patch((state) => {
state.count += 1;
// 注意:state 参数是 Store 本身,可以直接修改
});

3. getters:Store 的计算属性

在 Setup Store 中,getters 就是使用 Vue 的 computed() 定义的计算属性。它们依赖 state 或其他 getters,并在依赖变化时自动重新计算。

3.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// stores/cart.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

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

// 商品总数
const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
);

// 总价
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0).toFixed(2)
);

// 是否为空
const isEmpty = computed(() => items.value.length === 0);

return { items, totalItems, totalPrice, isEmpty };
});

3.2 在 getter 中访问其他 Store 的 getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// stores/order.js
import { defineStore } from 'pinia';
import { computed } from 'vue';
import { useCartStore } from './cart';
import { useUserStore } from './user';

export const useOrderStore = defineStore('order', () => {
const cart = useCartStore();
const user = useUserStore();

// 订单摘要:包含用户信息和购物车内容
const orderSummary = computed(() => ({
user: user.profile,
items: cart.items,
total: cart.totalPrice,
}));

const canCheckout = computed(() =>
user.profile !== null && !cart.isEmpty
);

return { orderSummary, canCheckout };
});

注意:在 getter(或 action)中访问其他 Store 时,直接在 setup 函数内部调用该 Store 的 useStore() 即可。由于这些代码在 Store 的 setup 函数中执行,它们遵循与组件相同的规则——可以自由使用其他 Store。

3.3 向 getter 传递参数

computed 本身不接受参数,但可以通过返回一个函数实现“带参数的 getter”:

1
2
3
4
5
const itemById = computed(() => {
return (id) => items.value.find(item => item.id === id);
});

// 使用:cart.itemById(42)

这种方式返回的是一个普通函数(不是响应式的 getter),但因为它依赖了 items.value,当 items 变化时,返回的函数会被重新创建。


4. actions:业务逻辑与异步操作

在 Setup Store 中,actions 就是普通的 JavaScript 函数(可以是同步或异步)。你可以直接在 action 中修改 state、调用其他 actions、或使用其他 Store。

4.1 同步 Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function addItem(product) {
const existing = items.value.find(item => item.id === product.id);
if (existing) {
existing.quantity += 1;
} else {
items.value.push({ ...product, quantity: 1 });
}
}

function removeItem(id) {
const index = items.value.findIndex(item => item.id === id);
if (index > -1) {
items.value.splice(index, 1);
}
}

function clearCart() {
items.value = [];
}

4.2 异步 Action

异步 action 直接使用 async/await,无需任何额外配置(不像 Vuex 需要使用 actions + mutations 的组合来处理异步):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ref } from 'vue';
import { defineStore } from 'pinia';

export const useProductStore = defineStore('product', () => {
const products = ref([]);
const loading = ref(false);
const error = ref(null);

async function fetchProducts() {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/products');
if (!response.ok) throw new Error('获取商品列表失败');
products.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}

return { products, loading, error, fetchProducts };
});

与 Vuex 的对比

  • Vuex 中异步操作必须放在 actions 中,通过 commit('mutation') 修改 state。同步修改必须在 mutations 中。
  • Pinia 中 actions 可以直接修改 state,无需 mutations。同步和异步操作统一在 actions(或直接在组件中)处理。这消除了 Vuex 中 actionsmutations 之间繁琐的类型映射和命名重复。

4.3 在 Action 中调用另一个 Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function checkout() {
const order = {
items: items.value,
total: totalPrice.value,
createdAt: new Date().toISOString(),
};

// 提交订单
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(order),
});

// 清空购物车(调用本 Store 的另一个 action)
clearCart();
}

5. 组合多个 Store:购物车完整实战

以下示例展示了 useProductStoreuseCartStore 如何在组件中配合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// stores/products.js
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useProductStore = defineStore('products', () => {
const products = ref([
{ id: 1, name: '笔记本电脑', price: 5999 },
{ id: 2, name: '无线耳机', price: 899 },
{ id: 3, name: '机械键盘', price: 499 },
{ id: 4, name: '显示器', price: 2299 },
]);

return { products };
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// stores/cart.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCartStore = defineStore('cart', () => {
const items = ref([]);

const totalItems = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
);

const totalPrice = computed(() =>
items.value
.reduce((sum, item) => sum + item.price * item.quantity, 0)
.toFixed(2)
);

const isEmpty = computed(() => items.value.length === 0);

function addItem(product) {
const existing = items.value.find(item => item.id === product.id);
if (existing) {
existing.quantity += 1;
} else {
items.value.push({ ...product, quantity: 1 });
}
}

function removeItem(id) {
items.value = items.value.filter(item => item.id !== id);
}

function updateQuantity(id, quantity) {
const item = items.value.find(item => item.id === id);
if (item) {
item.quantity = quantity;
if (item.quantity <= 0) removeItem(id);
}
}

function clearCart() {
items.value = [];
}

return { items, totalItems, totalPrice, isEmpty, addItem, removeItem, updateQuantity, clearCart };
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!-- ShoppingView.vue -->
<script setup>
import { useProductStore } from '@/stores/products';
import { useCartStore } from '@/stores/cart';

const productsStore = useProductStore();
const cart = useCartStore();
</script>

<template>
<div class="shopping-view">
<!-- 商品列表 -->
<section class="products">
<h2>商品列表</h2>
<div v-for="p in productsStore.products" :key="p.id" class="product-card">
<h3>{{ p.name }}</h3>
<p>¥{{ p.price }}</p>
<button @click="cart.addItem(p)">加入购物车</button>
</div>
</section>

<!-- 购物车 -->
<section class="cart">
<h2>购物车 ({{ cart.totalItems }} 件)</h2>
<div v-if="cart.isEmpty">
<p>购物车是空的</p>
</div>
<div v-else>
<div v-for="item in cart.items" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<span>¥{{ item.price }}</span>
<div class="quantity-controls">
<button @click="cart.updateQuantity(item.id, item.quantity - 1)">-</button>
<span>{{ item.quantity }}</span>
<button @click="cart.updateQuantity(item.id, item.quantity + 1)">+</button>
</div>
<button @click="cart.removeItem(item.id)">删除</button>
</div>
<div class="cart-summary">
<strong>总计:¥{{ cart.totalPrice }}</strong>
<button @click="cart.clearCart">清空购物车</button>
</div>
</div>
</section>
</div>
</template>

设计要点

  • products Store 是商品的“数据源”,保持简单——只有 state,无 getters 和 actions。
  • cart Store 封装了购物车的所有逻辑:addItemremoveItemupdateQuantityclearCart,以及三个派生值 totalItemstotalPriceisEmpty
  • 组件中同时使用两个 Store,各司其职,互不干扰。useCartStoreaddItem 中接收来自 useProductStore 的商品对象,两者通过函数参数解耦。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) Pinia 中 Setup Store 的 state 是通过什么定义的?
    A. data() 函数返回的对象。
    B. refreactive
    C. computed
    D. state() 配置函数。

  2. (单选) Pinia 与 Vuex 的一个重要区别是?
    A. Pinia 必须使用 mutations 修改 state。
    B. Pinia 不允许在 actions 中直接修改 state。
    C. Pinia 移除了 mutations,允许在 actions 中直接修改 state。
    D. Pinia 不提供 DevTools 支持。

  3. (填空) 在 Setup Store 中,getters 使用 Vue 的 ______ 函数定义。

  4. (多选) 关于 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
  • statebooks 数组(初始包含 3 本静态图书 { id, title, author, available })、searchQuery 字符串。
  • gettersfilteredBooks(根据 searchQuery 按书名模糊筛选,大小写不敏感)、availableBooks(仅返回 available === true 的书)、bookCount(总数量)。
  • actionsaddBook(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:booksref<Book[]> 初始化 3 本静态数据;searchQueryref('')
  • Getters(使用 computed):
    • filteredBooks:若 searchQuery.value 为空返回全部,否则返回 books.value.filter(b => b.title.toLowerCase().includes(searchQuery.value.toLowerCase()))
    • availableBooks:返回 filteredBooks.valueavailable === true 的项。
    • bookCount:返回 filteredBooks.value.length
  • Actions:
    • addBook(book: Omit<Book, 'id'>):生成 idDate.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 中 actionsmutations 之间命名重复、类型映射繁琐的问题。
  • 类型推断:Pinia 为 TypeScript 设计,Setup Store 中的 refcomputed 自动推导类型,无需额外类型标注。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> 心智模型完全一致——refstatecomputedgettersfunctionactions。开发者无需学习新的 API,可以直接复用 Vue 3 组合式 API 的全部知识。同时,可以将公共逻辑提取为 composable 函数在多个 Store 间共享。

选择 Pinia 而非 provide/inject 的场景:当数据需要在多个不相关的组件树之间共享(而非仅一个组件子树内)、需要持久化或插件扩展(如 pinia-plugin-persistedstate)、需要DevTools 调试、或需要复杂的异步操作和缓存时,Pinia 是更好的选择。provide/inject 适合组件树局部范围内的轻量共享,生命周期跟随提供者组件。


课后练习答案

一、概念自测答案

  1. B

    • 解析:Setup Store 使用 refreactive 定义 state。A 是选项式 API 的 data,C 是 getters,D 是 Options Store 的 state 函数。
  2. C

    • 解析:Pinia 移除了 mutations,允许在 actions 中直接修改 state。A 和 B 描述的是 Vuex,D 错误(Pinia 内置 DevTools 支持)。
  3. computed

    • 解析:Setup Store 使用 Vue 的 computed 函数定义 getters。
  4. A、B、D

    • 解析:C 错误,useStore() 在同一组件或不同组件中返回同一个单例实例。A、B、D 均为正确描述。

二、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。输出完整文件。”
CATALOG
  1. 1. 第110课:Pinia 状态管理(上)——Store 定义、state、getters、actions
    1. 1.1. 1. 安装 Pinia 与创建第一个 Store
      1. 1.1.1. 1.1 安装与挂载
      2. 1.1.2. 1.2 定义 Store:两种语法
      3. 1.1.3. 1.3 在组件中使用 Store
    2. 1.2. 2. state:响应式数据的核心
      1. 1.2.1. 2.1 ref vs reactive 的选择
      2. 1.2.2. 2.2 重置 State
      3. 1.2.3. 2.3 直接修改 State 与 $patch
    3. 1.3. 3. getters:Store 的计算属性
      1. 1.3.1. 3.1 基本用法
      2. 1.3.2. 3.2 在 getter 中访问其他 Store 的 getter
      3. 1.3.3. 3.3 向 getter 传递参数
    4. 1.4. 4. actions:业务逻辑与异步操作
      1. 1.4.1. 4.1 同步 Action
      2. 1.4.2. 4.2 异步 Action
      3. 1.4.3. 4.3 在 Action 中调用另一个 Action
    5. 1.5. 5. 组合多个 Store:购物车完整实战
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)