04|状态修改、patch 与订阅
第五章概览 / 04。目标:让一次业务动作对应一段可解释的状态变化;“批处理”不等于数据库事务,也不自动带来性能提升。
Pinia 允许的三类写入
const cart = useCartStore()
cart.error = '请检查购物车' // 1. 直接修改
cart.$patch({ // 2. 对象 patch
error: '',
})
cart.$patch(state => { // 3. 函数 patch
state.lines.push({ productId: 'book-1', unitPrice: 59, quantity: 1 })
state.error = ''
})三者都会被 Pinia 的响应式系统和开发工具观察到。选择标准不是“哪种更高级”:
- 单字段、无业务不变量的 UI 写入可以直接修改。
- 一组浅层字段替换可用对象
$patch,mutation 记录里能看到 payload。 - 集合 push/splice 或多步计算适合函数
$patch,避免先构造整个新数组。 - 跨字段约束、复用逻辑、异步请求和错误处理放 action;action 内仍可直接写或 patch。
一个动作怎样维护不变量
export const useProfileStore = defineStore('profile', {
state: () => ({
name: '',
email: '',
revision: 0,
error: '',
}),
actions: {
applyProfile(input: { name: string; email: string }) {
const name = input.name.trim()
const email = input.email.trim().toLowerCase()
if (!name || !email.includes('@')) {
this.error = '姓名或邮箱无效'
return false
}
this.$patch({
name,
email,
revision: this.revision + 1,
error: '',
})
return true
},
},
})校验发生在写入前,因此失败不会留下“姓名已改、邮箱还没改”的半状态。$patch 把这组变化归在一次 mutation 记录里,但它没有回滚能力:若回调中途抛错,已经执行的赋值不会自动撤销。需要原子性时,应先在局部变量完成计算和校验,再一次提交。
$subscribe() 观察的是状态提交
const stop = profile.$subscribe(
(mutation, state) => {
console.debug(mutation.storeId, mutation.type, state.revision)
},
{ flush: 'sync' },
)
// 不再需要时显式停止
stop()mutation 的 type 可区分 direct、patch object、patch function;只有对象 patch 有 payload。函数 $patch 中多次写入通常只触发一次订阅回调,这有利于持久化和日志归组,却不能证明组件只渲染一次,也不能代替性能测量。
$subscribe() 基于 Vue watch,可传 flush 等选项。组件 setup 中注册的订阅默认跟组件生命周期绑定;传 { detached: true } 后会脱离组件,必须保存返回的停止函数并由应用级 owner 清理。
reset 的边界
Option Store 可调用自动 $reset(),Pinia 会重新执行 state() 得到初始状态:
profile.$reset()Setup Store 没有自动 $reset(),因为 Pinia 不知道哪些 ref 应恢复成什么;应提供领域动作:
function reset() {
lines.value = []
error.value = ''
}重置客户端状态不等于撤销服务端写入。若保存请求已经成功,$reset() 只清本地内存;要撤销业务操作必须调用服务端提供的补偿接口。
$state 与替换错觉
store.$state = { count: 24 }Pinia 为了不破坏响应式连接,会把这种“替换”转换为 patch。不要依赖替换对象身份来通知消费者。SSR hydration 可以给 pinia.state.value 提供初始树,但必须在创建/读取 stores 之前完成,并安全序列化服务端数据。
常见失败路径
- 把所有写入都包 action,却让 action 只做单行转发:增加名字,没有增加不变量或可观察性。
- 把
$patch当事务:回调抛错后误以为自动回滚。 - 在订阅里再次写同一 store:可能形成反馈循环。持久化回调只写外部存储,不反向修正 state。
- 用订阅保存密码、token 或完整用户对象:日志和 storage 扩大敏感数据暴露面。
- 应用级 detached 订阅没有停止:测试间泄漏回调,热更新时重复记录。
练习:原子更新个人资料
为上面的 useProfileStore 补充 applyServerSnapshot(snapshot, expectedRevision):只有 expectedRevision === state.revision 且数据合法时,才同时更新姓名、邮箱和 revision;否则返回 false,原 state 完全不变。
snapshot为{ name:string, email:string, revision:number };姓名去空格后非空、邮箱含@,且snapshot.revision必须是大于当前revision的安全整数。expectedRevision冲突或任一输入不合法均不得写error或其他state字段。把可能抛错的计算放在提交前;本题不要求patch具备回滚能力。
再注册一次 $subscribe,记录 mutation 次数和类型。验收:成功快照只产生一次 patch 订阅;非法邮箱、revision 冲突、提交前计算抛错都不会留下半状态;练习结束调用停止函数。
参考答案:第 04 题。
本页结论
写入 API 决定记录的形状,action 决定业务意图,真正的原子性来自“先验证、后提交”。订阅必须有 owner 和清理点。