09|状态调试
第五章概览 / 09。目标:从“最终值不对”退回完整因果链,判断错误发生在输入、写入、异步竞态、派生依赖还是渲染。
先记录一条最小时间线
点击保存
→ profile.save({ revision: 3 })
→ PATCH /api/profile(requestId=p-17)
→ 用户切换到 revision 4
→ p-17 返回
→ 旧响应是否被忽略?只打印最终 $state 看不到“谁先发生”。复现记录至少包含:初始 state、用户输入、action 名与参数、请求 id/开始结束、mutation 类型、最终可见结果。敏感字段先脱敏。
用 $onAction() 观察意图
const stopActions = store.$onAction(({ name, args, after, onError }) => {
const startedAt = performance.now()
console.debug('[action:start]', store.$id, name, sanitize(args))
after(result => {
console.debug('[action:end]', store.$id, name, {
durationMs: performance.now() - startedAt,
result: sanitize(result),
})
})
onError(error => {
console.error('[action:error]', store.$id, name, {
durationMs: performance.now() - startedAt,
message: error instanceof Error ? error.message : String(error),
})
})
})after 会等待 action 返回的 Promise;onError 观察抛出的异常。它适合定位“哪个业务动作”触发变化。调用完成后不会自动停止应用级 detached 监听,所以 owner 必须保存 stopActions。
用 $subscribe() 观察提交
const stopMutations = store.$subscribe((mutation, state) => {
console.debug('[mutation]', {
storeId: mutation.storeId,
type: mutation.type,
revision: state.revision,
})
}, { detached: true, flush: 'sync' })订阅能看到 direct/object patch/function patch,但不能告诉你 direct mutation 属于哪个用户意图;所以 action 时间线和 mutation 时间线解决不同问题。不要永久记录整个 state,尤其不要复制 token、表单内容或大列表。
Vue DevTools 看什么
- 在组件树确认当前组件实际消费哪个 store。
- 在 Pinia 面板检查 store id、state/getter 与 action 时间线。
- 用时间旅行回到某个 mutation,确认 bug 是输入已错,还是后续动作覆盖。
- 重放时把网络、时间、随机数等外部输入固定;纯 state 快照无法重放服务端副作用。
时间旅行只是开发诊断,不是业务撤销。数据库写入、localStorage 和已经发送的埋点不会随 state 快照自动回滚。
响应式依赖调试
当 mutation 正确而视图没更新,检查是否错误解构 store;当视图更新过多,开发模式可用响应式调试钩子:
const total = computed(
() => store.lines.reduce((sum, line) => sum + line.quantity, 0),
{
onTrack(event) { console.debug('tracked', event.key) },
onTrigger(event) { console.debug('triggered', event.type, event.key) },
},
)computed/watch/watchEffect 的 onTrack/onTrigger 以及组件的 onRenderTracked/onRenderTriggered 只用于开发调试。它们显示响应式 get/set/add/delete 等触发关系,不替代性能分析。定位后删除临时日志,或把发现的输入序列固化成测试。
快照要可比较、可恢复
调试快照只选纯数据字段,并在比较前规范化时间、排序等不稳定值。Vue reactive proxy、函数、AbortController、DOM 节点不能当作可移植快照。恢复快照也要走明确入口,避免绕过新版本 schema。
function snapshotCart(store: ReturnType<typeof useCartStore>) {
return {
lines: store.lines.map(line => ({
productId: line.productId,
unitPrice: line.unitPrice,
quantity: line.quantity,
})),
}
}这里逐行投影的是购物车正文已经声明的三个原始字段,生成普通数组和普通对象。直接把 store.lines 放进 structuredClone 会因它仍是 Proxy 而抛 DataCloneError;只把最外层包成 { lines } 也不能去掉内部代理。验证时修改 snapshot.lines[0].quantity,store 中对应数量必须保持原值。若后续增加嵌套字段,应继续按 schema 投影,不能假定一次浅复制足够。
排错顺序
- 固定最小初始状态和输入,确认可重复。
- 查 owner:是否有重复副本或错误 store 实例。
- 查 action 与 mutation 顺序,标注请求身份。
- 查缓存 key、失效和旧响应覆盖。
- 最后才查渲染依赖与性能;不要用更多 watch 掩盖上游错误。
- 修复后让原复现失败、正确路径通过,并停止/删除诊断监听。
练习:定位旧响应覆盖
给一个 search(term) action:连续搜索 vue 和 pinia,前者后返回并覆盖后者。要求写可控 Promise 复现;用 $onAction 记录 action 起止,用 request id 或 AbortController 修复;最终结果必须是 pinia,旧请求不得把 status 改回 success/error。
验收还包括:日志不含完整响应和用户输入;两类停止函数在测试结束调用;修复后的测试能在颠倒完成顺序时稳定通过。
参考答案:第 09 题。
本页结论
状态调试不是多打几个 console.log,而是重建 owner 与时序。action 表示意图,mutation 表示提交,请求 id 表示并发,响应式钩子表示视图依赖。