Vue3 零基础每日笔记(055):组件里正确使用 store——storeToRefs 解构与 $patch 批量修改

一、前言

前两篇一直用 store.count 整体访问,能跑但不够讲究。本篇解决四个实战问题:为什么解构 store 会丢响应式?actions 能不能解构?$patch 和直接改有什么区别?怎么监听 state 变化? 坑的根源和 012 篇 toRefs 是同一个:响应式对象被解构,连接就断了。

二、解构 store:直接解构必踩坑

2.1 复现:页面不更新

vue 复制代码
<template>
  <!-- ❌ 点了按钮 count 永远是 0 -->
  <p>count:{{ count }}</p>
  <button @click="store.increment()">+1</button>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
const { count } = store   // ❌ 解构瞬间取走了"当时的值",断开响应式连接
</script>

原因:store 本身是响应式对象,const { count } = store 把 count 变成一个普通数字,和 012 篇"解构 reactive 丢响应式"完全同款。

2.2 正确姿势:storeToRefs

vue 复制代码
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()

// ★ state 和 getters 用 storeToRefs 解构,保持响应式
const { count, double } = storeToRefs(store)

// ★ actions 是函数,不依赖响应式包裹,直接解构
const { increment } = store
</script>

<template>
  <p>count:{{ count }}</p>
  <p>double:{{ double }}</p>
  <button @click="increment()">+1</button>
</template>

记忆口诀:数据用 storeToRefs,方法直接拿。storeToRefs 只会包含 state 和 getters,把 actions 传进去也没用(这正是它替你过滤好的)。

三、修改 state 的四种方式

以购物车 store 为例,演示四种改法:

ts 复制代码
// src/stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export interface CartItem {
  id: number
  name: string
  price: number
  count: number
}

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const vip = ref(false)

  const totalPrice = computed(() =>
    items.value.reduce((s, i) => s + i.price * i.count, 0)
  )

  // 参数不需要 count:store 内部统一补 1(Omit 表示"排除某字段")
  function addItem(item: Omit<CartItem, 'count'>) {
    const exist = items.value.find((i) => i.id === item.id)
    if (exist) exist.count++
    else items.value.push({ ...item, count: 1 })
  }

  function clear() {
    items.value = []
    vip.value = false
  }

  return { items, vip, totalPrice, addItem, clear }
})

组件里四种改法对比:

ts 复制代码
const store = useCartStore()

// ① 直接改:简单直观,适合改一两个字段
store.vip = true

// ② $patch 对象形式:一次改多个字段(比多次直接改触发一次更新)
store.$patch({ vip: true })

// ③ $patch 函数形式:★ 多处复杂修改的正确姿势
store.$patch((state) => {
  state.items.push({ id: 3, name: '键盘', price: 199, count: 1 })
  state.vip = true
})

// ④ action 收口:业务逻辑进 store,组件只管调方法(最推荐)
store.addItem({ id: 4, name: '鼠标', price: 99 })

**patch的真正价值∗∗:把"多处修改"合并成一次响应式更新。如果对同一个store连续改5个字段,直接改会触发5次更新流程,patch 的真正价值**:把"多处修改"合并成一次响应式更新。如果对同一个 store 连续改 5 个字段,直接改会触发 5 次更新流程,patch的真正价值∗∗:把"多处修改"合并成一次响应式更新。如果对同一个store连续改5个字段,直接改会触发5次更新流程,patch 只触发 1 次。日常改一两个字段用直接改就行,别为了 patch 而 patch。

完整组件代码:

vue 复制代码
<!-- src/views/CartView.vue -->
<template>
  <p>是否 VIP:{{ vip ? '是' : '否' }},合计:{{ store.totalPrice }} 元</p>
  <ul>
    <li v-for="i in items" :key="i.id">{{ i.name }} × {{ i.count }}</li>
  </ul>
  <button @click="addKeyboard">函数式 $patch 加键盘</button>
  <button @click="store.addItem({ id: 4, name: '鼠标', price: 99 })">action 加鼠标</button>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'

const store = useCartStore()
const { items, vip } = storeToRefs(store)   // 数据解构保持响应式

function addKeyboard() {
  store.$patch((state) => {
    state.items.push({ id: 3, name: '键盘', price: 199, count: 1 })
    state.vip = true
  })
}
</script>

【截图位置:两种添加方式的渲染结果一致,合计金额随 VIP 状态联动】

四、reset 与 subscribe

4.1 $reset:一键回到初始值

ts 复制代码
// Options Store:内置直接用
store.$reset()

// Setup Store:自己写 reset action(053 篇提过)
function reset() {
  items.value = []
  vip.value = false
}

4.2 $subscribe:监听整个 store 的变化

ts 复制代码
// 组件 <script setup> 里
store.$subscribe((mutation, state) => {
  console.log('变化类型:', mutation.type)       // direct / patch object / patch function
  console.log('最新完整 state:', state.items)
  // 典型用途:购物车变了就同步到本地
  localStorage.setItem('cart', JSON.stringify(state.items))
})

默认订阅跟随组件销毁而停止;想让订阅脱离组件持续存在(比如全局日志),加第二个参数:

ts 复制代码
store.$subscribe(callback, { detached: true })

五、踩坑记录

  1. 解构 state/getters 不用 storeToRefs:页面不更新,且控制台不报错,纯静默坑------看到"数据变了界面没变"先查解构
  2. 用 storeToRefs 解构 actions :拿不到或行为异常------actions 是函数直接 const { addItem } = store
  3. **patch函数参数误当ref用∗∗:回调里的'state.items.push(...)'不要写'state.items.value.push'------patch 函数参数误当 ref 用**:回调里的 `state.items.push(...)` 不要写 `state.items.value.push`------patch函数参数误当ref用∗∗:回调里的'state.items.push(...)'不要写'state.items.value.push'------patch 给你的是已解包的代理对象
  4. 跨组件 ref 命名冲突 :组件里 const { items } = storeToRefs(store) 和本地 items 重名------重命名 items: cartItems 或改本地变量名
  5. $subscribe 忘了 detached :订阅写在父组件却希望子页面跳转后仍然生效------组件卸载订阅就停了,加 { detached: true }
  6. **reset清不掉持久化数据∗∗:reset 清不掉持久化数据**:reset清不掉持久化数据∗∗:reset 只重置内存 state,持久化插件(057 篇)存的数据要另外处理

六、今日小结

  • 解构口诀:数据用 storeToRefs,方法直接拿(和 012 篇 toRefs 一个思路)
  • 修改 state 四方式:直接改(少量字段)→ $patch(批量合并一次更新)→ action(业务收口,首选)
  • $reset:Options 内置,Setup 手写
  • $subscribe 监听整个 store,detached: true 可脱离组件存活

下篇预告

状态一多,一个 store 文件几百行就灾难了。怎么按业务域拆分 user / cart / goods 多个 store?它们之间怎么安全互相调用、怎么避免循环引用?下一篇 056 Pinia 模块化拆分与互相调用。

相关推荐
kkkkkkkkkk_Z1 小时前
新手自学嵌入式|学习日记:从C语言到51单片机基础(GPIO、中断、定时器与PWM篇)
linux·笔记·学习·51单片机
陈年老古董1 小时前
微博情感分析项目学习笔记
笔记·python·学习·机器学习·项目
2601_960631231 小时前
如何用公开数据接口查询制药企业资质信息
笔记
yzy852 小时前
Vue中下载或打开文件的JavaScript方法
前端·javascript·vue
小刘在重生~2 小时前
Oracle_day2 单行函数|组函数|分组|伪列|子查询|分页|多表连接
笔记·oracle·面试
AAA代码批发商3 小时前
Days 47 IMX6ULL 裸机开发:按键轮询实验 + 外部中断原理剖析(上)
笔记·学习·imx6ull·裸机开发·#arm中断
sunoo-2293 小时前
【ARM嵌入式学习笔记Day6】一文打通i.MX6ULL时钟体系:PLL/PFD/预分频+滞回比较器全解析
arm开发·笔记·vscode·单片机·imx6ull·汇编语言
吴声子夜歌3 小时前
HTML——看似普通的元素的背后(<a>标签)
html
IMPYLH3 小时前
HTML 的 <style> 元素
前端·html