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

一、前言

上一篇把 Pinia 跑通了"hello world",本篇把三件套一次性讲透:state 怎么改才规范、getters 怎么串联和传参、actions 怎么发异步请求。学完这篇,普通业务里的 store 就够用了。代码延续 Setup Store 写法(053 篇结论:组合式是主流),Options 写法同步对照。

二、state:数据的家,三种修改姿势

state 就是数据本体:Options 里是 state: () => ({...}),Setup 里就是一个个 ref。

修改 state 实际有三种姿势,先混个脸熟:

text 复制代码
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:依赖变了才重算,否则用缓存。

ts 复制代码
// Setup 写法:getters 就是 computed
const scores = ref([60, 80])
const totalScore = computed(() => scores.value.reduce((s, n) => s + n, 0))
ts 复制代码
// 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 下必须手写返回值类型,否则类型循环推导报错:

ts 复制代码
getters: {
  double: (state) => state.count * 2,

  // ★ 用了 this → 不能写箭头函数;TS 下必须标注返回类型 number
  doublePlusOne(): number {
    return this.double + 1
  }
}

Setup 写法没这个问题,computed 之间互相引用非常自然:

ts 复制代码
const double = computed(() => count.value * 2)
const level = computed(() => double.value >= 100 ? '高级' : '初级')

这也是推荐 Setup Store 的原因之一:心智和组件完全一致。

3.3 getter 传参:返回一个函数

想写"带参数的计算属性",让 getter 返回一个函数:

ts 复制代码
// Options 写法
getters: {
  getTodoById: (state) => {
    // ★ 返回的是函数,调用时才传参
    return (id: number) => state.todos.find((t) => t.id === id)
  }
}

// 组件里:store.getTodoById(3)
ts 复制代码
// 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

ts 复制代码
function addScore(n: number) {
  scores.value.push(n)
}

4.2 异步 action(重点)

action 就是普通函数,async/await 随便用,还能有返回值给组件接:

ts 复制代码
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

ts 复制代码
function registerAndLogin() {
  // 调自己 store 的 action
  addScore(10)
}

async function checkout() {
  // 调别的 store:直接 useXxxStore()(模块化详见 056 篇)
  const cartStore = useCartStore()
  cartStore.clear()
}

五、完整可运行代码:用户登录 + 积分等级

ts 复制代码
// 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 }
})
vue 复制代码
<!-- 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 分后等级从初级变高级】

六、踩坑记录

  1. getter 用 this 却写成箭头函数 :箭头函数没有自己的 this,this.double 直接 undefined------Options 里"用 this 的 getter"必须写普通函数
  2. getter 用 this 忘标返回类型 :TS 下报"xx 隐式具有 any 类型/循环引用"------返回类型是简单类型就手写上 (): number
  3. 在 getter 里发请求:getter 应该是纯计算,副作用(请求、定时器)放 action;getter 里发请求会导致难以追踪的重复请求
  4. Setup Store 调 $reset() 报错 :内置 $reset 只有 Options Store 有------Setup 自己写:function reset() { token.value = ''; name.value = '' }
  5. 异步 action 不加 loading/错误处理:请求失败界面毫无反馈------action 内 try/catch 或让组件 await 时自己处理,状态字段(loading)进 state
  6. 传参 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。

相关推荐
Caroline5163 小时前
GPT Image 2.5还能这么玩!一套提示词生成IP吉祥物,附完整案例
前端·gpt·tcp/ip
ym hyd 1113 小时前
慢性病精细化管理平台源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设
wangyadong3173 小时前
# el-table 复选框无法选中问题记录
前端·javascript·vue.js
葡萄城技术团队3 小时前
给 SpreadJS 单元格装上自动完成
前端
恋猫de小郭3 小时前
Flutter 多窗口重要优化合并,多窗口性能和实用性大幅提升
android·前端·flutter
StudyAndLife3 小时前
开源一个 API 开放平台门户前端:本地 npm run dev 就能逛接口文档
前端
广州华水科技3 小时前
GNSS变形监测一体机在大坝安全监测中的应用与优势分析
前端
hunteritself4 小时前
夯爆了!Qoder 狂肝 2 小时,293 个测试全绿,Credits 一分没扣
前端·人工智能·chrome·深度学习·机器学习
Hilaku4 小时前
为什么前端应该主动去学数据库?
前端·javascript·程序员