Pinia

State 修改与响应式细节

深入理解 Pinia 中 state 的修改方式,包括直接修改、$patch、$reset,以及响应式数组和对象的注意事项。

🎯 引言

学会这篇文章,你能掌握 Pinia 中修改 state 的三种方式,理解它们各自适合什么场景。同时你还会避开对象和数组操作中的响应式陷阱,让状态管理更稳。


✨ 直接修改:最简单

Pinia 允许直接修改 state,这是它和 Vuex 最大的不同之一。

stores/counter.js
export const useCounterStore = defineStore('counter', {
    state: () => ({
        count: 0,
    }),
});

在组件中:

<template>
    <p>{{ counter.count }}</p>
    <button @click="counter.count++">加一</button>
    <button @click="counter.count = 0">重置</button>
</template>
单个字段的修改用直接赋值最直观。但如果一次要改很多字段,或者修改逻辑比较复杂,就该考虑 $patch 或 action。

🛠 $patch:批量修改

当一次需要修改多个 state 字段时,用 $patch 更清晰:

counter.$patch({
    count: 10,
    name: '李四',
});

$patch 也支持函数形式,适合需要基于当前状态计算的场景:

counter.$patch((state) => {
    state.count++;
});
函数形式的 $patch 接收当前 state 作为参数,在里面可以直接改。这样写逻辑更集中,也更容易追踪。

🔄 $reset:一键恢复初始值

如果你想把 state 恢复到 state() 函数里写的初始值,可以调用 $reset

counter.$reset(); // count 变回 0
$reset 只对通过 state: () => ({}) 定义的初始状态有效。如果你之前用 $patch 整体替换了某个对象字段,恢复行为可能不符合预期。

⚡ 响应式对象和数组的陷阱

替换整个对象

下面这种写法会破坏响应式:

const user = useUserStore();
user.info = { name: '李四', age: 25 }; // ❌ 可能丢失响应式

为什么?因为 info 的引用被整个换掉了,Pinia 内部对这个字段的响应式追踪可能会出问题。

正确做法:

// ✅ 逐个改属性
user.info.name = '李四';
user.info.age = 25;

// ✅ 或者用 $patch
user.$patch({
    info: { name: '李四', age: 25 },
});

数组操作

数组的 pushsplice 等方法都能保持响应式:

const list = useListStore();

list.items.push('新元素'); // ✅
list.items.splice(0, 1); // ✅

但下面这种直接通过索引替换整个对象要小心:

list.items[0] = { id: 2, name: '' }; // ⚠️ 可能不触发更新

更安全的写法:

list.$patch((state) => {
    state.items[0] = { id: 2, name: '' };
});
Pinia 的 state 虽然响应式,但 JavaScript 对象和数组的某些赋值方式会让响应式追踪失效。遇到不确定的写法,优先用 $patch

💡 三种修改方式怎么选

方式适用场景例子
直接修改单个字段简单赋值counter.count++
$patch批量修改、复杂对象/数组操作counter.$patch({...})
action复杂业务逻辑、多处复用counter.add()

🧾 小节总结

  • Pinia 支持直接修改 state,适合简单场景。
  • $patch 适合批量修改和复杂对象/数组操作。
  • $reset 可以把 state 恢复到初始值。
  • 替换整个对象引用可能导致响应式丢失,建议用 $patch 或逐个修改属性。

❓ 知识问答

Q1:直接修改和 $patch 哪个更好?

没有绝对好坏。单个字段用直接修改更简洁,多个字段或复杂操作用 $patch 更清晰。

Q2:$reset 能重置 action 里修改过的状态吗?

能。$reset 只认 state() 函数返回的初始值,不管中间被什么方式修改过。

Q3:为什么替换整个对象会丢失响应式?

因为响应式系统追踪的是对象引用。你把引用换成一个新的普通对象,原来的响应式代理就断开了。

Q4:数组通过索引赋值一定失效吗?

不一定,但行为不如 push/splice/$patch 可靠。为了避免踩坑,建议用稳妥的写法。


🧪 小练习

补全下面的 usePointStore,实现:

  1. points 初始为 0
  2. add(n) action 给 pointsn
  3. consume(n) action 消费积分,积分不足时不扣减。
  4. reset() action 把积分重置为 0
stores/point.js
import { defineStore } from 'pinia';

export const usePointStore = defineStore('point', {
    state: () => ({
        points: 0,
    }),
    actions: {
        // 请在这里编写代码
    },
});

🎉 恭喜你掌握了 state 修改的各种方式!下一篇我们学习 actions 中处理异步请求。