一、前言
前两篇一直用 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 })
五、踩坑记录
- 解构 state/getters 不用 storeToRefs:页面不更新,且控制台不报错,纯静默坑------看到"数据变了界面没变"先查解构
- 用 storeToRefs 解构 actions :拿不到或行为异常------actions 是函数直接
const { addItem } = store - **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 给你的是已解包的代理对象
- 跨组件 ref 命名冲突 :组件里
const { items } = storeToRefs(store)和本地items重名------重命名items: cartItems或改本地变量名 - $subscribe 忘了 detached :订阅写在父组件却希望子页面跳转后仍然生效------组件卸载订阅就停了,加
{ detached: true } - **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 模块化拆分与互相调用。