一、前言
上一篇把 Pinia 跑通了"hello world",本篇把三件套一次性讲透:state 怎么改才规范、getters 怎么串联和传参、actions 怎么发异步请求。学完这篇,普通业务里的 store 就够用了。代码延续 Setup Store 写法(053 篇结论:组合式是主流),Options 写法同步对照。
二、state:数据的家,三种修改姿势
state 就是数据本体:Options 里是 state: () => ({…}),Setup 里就是一个个 ref。
修改 state 实际有三种姿势,先混个脸熟:
1. 直接改:store.count++ → 简单场景随手用
2. $patch 批量改:store.$patch({…}) → 一次改多处时用(055 篇细讲)
3. action 里改:把修改逻辑收口成方法 → ★ 推荐,业务代码都走这里
为什么推荐收口进 action?和组件里"不要到处散落改数据"一个道理:以后排查"谁把数据改坏的",只需要搜索 action 名,而不是全项目搜 store.xxx =。
另外 Pinia 给 Options Store 内置了 $reset()(一键重置回初始值);Setup Store 没有内置 $reset,要自己写个 reset action(踩坑 4)。
三、getters:全局版 computed
3.1 基础用法与缓存
getters 相当于 store 里的 computed:依赖变了才重算,否则用缓存。
// Setup 写法:getters 就是 computed
const scores = ref([60, 80])
const totalScore = computed(() => scores.value.reduce((s, n) => s + n, 0))
// Options 写法对照
getters: {
totalScore: (state) => state.scores.reduce((s: number, n: number) => s + n, 0)
}
3.2 getter 串联:用 this 引用别的 getter(Options 特有坑)
Options 写法里,getter 想用另一个 getter 的结果,不能用箭头函数,要写成普通函数用 this;并且 TS 下必须手写返回值类型,否则类型循环推导报错:
getters: {
double: (state) => state.count * 2,
// ★ 用了 this → 不能写箭头函数;TS 下必须标注返回类型 number
doublePlusOne(): number {
return this.double + 1
}
}
Setup 写法没这个问题,computed 之间互相引用非常自然:
const double = computed(() => count.value * 2)
const level = computed(() => double.value >= 100 ? '高级' : '初级')
这也是推荐 Setup Store 的原因之一:心智和组件完全一致。
3.3 getter 传参:返回一个函数
想写"带参数的计算属性",让 getter 返回一个函数:
// Options 写法
getters: {
getTodoById: (state) => {
// ★ 返回的是函数,调用时才传参
return (id: number) => state.todos.find((t) => t.id === id)
}
}
// 组件里:store.getTodoById(3)
// Setup 写法
const getTodoById = computed(() => {
return (id: number) => todos.value.find((t) => t.id === id)
})
注意:传参之后缓存特性就失效了(每次调用都重新执行一遍 find)。它只是"长得像 getter",本质是返回函数。如果某场景对性能敏感,改用 action 存结果,而不是滥用传参 getter。
四、actions:同步异步一把梭
Vuex 时代同步走 mutation、异步走 action;Pinia 里 全放 action,概念直接减半。
4.1 同步 action
function addScore(n: number) {
scores.value.push(n)
}
4.2 异步 action(重点)
action 就是普通函数,async/await 随便用,还能有返回值给组件接:
async function login(account: string, pwd: string) {
// 真实项目:const res = await axios.post('/api/login', { account, pwd })
const res = await new Promise<{ token: string; name: string }>((resolve) =>
setTimeout(() => resolve({ token: 'fake-token-123', name: account }), 800)
)
token.value = res.token
name.value = res.name
return token.value // ★ 返回值,组件里 await 拿到
}
4.3 action 调 action、调别的 store
function registerAndLogin() {
// 调自己 store 的 action
addScore(10)
}
async function checkout() {
// 调别的 store:直接 useXxxStore()(模块化详见 056 篇)
const cartStore = useCartStore()
cartStore.clear()
}
五、完整可运行代码:用户登录 + 积分等级
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// ———- state ———-
const name = ref('')
const token = ref('')
const scores = ref<number[]>([60, 80])
// ———- getters ———-
const totalScore = computed(() => scores.value.reduce((s, n) => s + n, 0))
const level = computed(() => (totalScore.value >= 140 ? '高级' : '初级'))
// ———- actions ———-
function addScore(n: number) {
scores.value.push(n)
}
async function login(account: string, pwd: string) {
const res = await new Promise<{ token: string; name: string }>((resolve) =>
setTimeout(() => resolve({ token: 'fake-token-123', name: account }), 800)
)
token.value = res.token
name.value = res.name
return token.value
}
function logout() {
token.value = ''
name.value = ''
}
return { name, token, scores, totalScore, level, addScore, login, logout }
})
<!– src/views/UserView.vue –>
<template>
<p>用户:{{ store.name || '未登录' }},等级:{{ store.level }}</p>
<p>总分:{{ store.totalScore }}</p>
<button @click="store.addScore(50)">加 50 分</button>
<button @click="onLogin" :disabled="loading">{{ loading ? '登录中…' : '登录' }}</button>
<button @click="store.logout()">退出</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore } from '@/stores/user'
const store = useUserStore()
const loading = ref(false)
async function onLogin() {
loading.value = true
const token = await store.login('陈同学', '123456')
console.log('拿到的 token:', token)
loading.value = false
}
</script>
【截图位置:点击登录 800ms 后显示用户名与 token;加 50 分后等级从初级变高级】
六、踩坑记录
七、今日小结
- state 修改三姿势:直接改 / $patch / action 收口,业务逻辑推荐全走 action
- getters = 全局 computed:Setup 用 computed 自然串联;Options 用 this 串联(普通函数 + 手标返回类型)
- 传参 getter = 返回函数,缓存失效,别当性能手段
- actions 同步异步一把梭:async/await + 返回值,跨 store 直接 useXxxStore()
下篇预告
组件里访问 store 的正确姿势:为什么解构 store 页面就不更新了?patch和直接改有什么区别?patch 和直接改有什么区别?patch和直接改有什么区别?subscribe 怎么监听全局状态?下一篇 055 组件中使用 store。



