欢迎光临
我们一直在努力

Vue3 零基础每日笔记(054):Pinia 三件套详解——state / getters / actions 全搞懂

一、前言

上一篇把 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 分后等级从初级变高级】

六、踩坑记录

  • getter 用 this 却写成箭头函数:箭头函数没有自己的 this,this.double 直接 undefined——Options 里"用 this 的 getter"必须写普通函数
  • getter 用 this 忘标返回类型:TS 下报"xx 隐式具有 any 类型/循环引用"——返回类型是简单类型就手写上 (): number
  • 在 getter 里发请求:getter 应该是纯计算,副作用(请求、定时器)放 action;getter 里发请求会导致难以追踪的重复请求
  • Setup Store 调 $reset() 报错:内置 $reset 只有 Options Store 有——Setup 自己写:function reset() { token.value = ''; name.value = '' }
  • 异步 action 不加 loading/错误处理:请求失败界面毫无反馈——action 内 try/catch 或让组件 await 时自己处理,状态字段(loading)进 state
  • 传参 getter 当缓存用:getTodoById(3) 每次都重新查找,没有缓存——高频调用场景换成 action 存结果
  • 七、今日小结

    • state 修改三姿势:直接改 / $patch / action 收口,业务逻辑推荐全走 action
    • getters = 全局 computed:Setup 用 computed 自然串联;Options 用 this 串联(普通函数 + 手标返回类型)
    • 传参 getter = 返回函数,缓存失效,别当性能手段
    • actions 同步异步一把梭:async/await + 返回值,跨 store 直接 useXxxStore()

    下篇预告

    组件里访问 store 的正确姿势:为什么解构 store 页面就不更新了?patch和直接改有什么区别?patch 和直接改有什么区别?patch和直接改有什么区别?subscribe 怎么监听全局状态?下一篇 055 组件中使用 store。

    赞(0)
    未经允许不得转载:171主机测评 » Vue3 零基础每日笔记(054):Pinia 三件套详解——state / getters / actions 全搞懂
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址