创建日期:2026-09-08 | 最近更新:2026-09-08 本机 pinia 4.0.3 + vue 3.5.42 实测;
[实测]输出真实。上一篇:组合式 store 与多 store。
解构、订阅与插件:Pinia 的“副作用”三件套
一句话:「读」用
storeToRefs(解构仍响应)、「听」用$subscribe/$onAction(状态/动作的通知钩子)、「扩展」用 pinia 插件(给所有 store 加能力)。本片把三个都实测一遍,并手写一个持久化插件看透机制。
1. storeToRefs:解构不丢响应性
前面反复用到,这里一次说清:store 本身是响应式代理,但 JS 的解构只会拷走那一刻的值。storeToRefs(store) 帮你把 state/getters 逐个转成 ref,解构后仍是响应式;actions 不是 ref,原样保留,所以只管 state 属性:
import { storeToRefs } from 'pinia';
const { count, double } = storeToRefs(store); // state 与 getters
const inc = store.inc; // action 直接解构没问题
- 不要用 Vue 的
toRefs(store)替代:toRefs会把 action 也当普通属性处理、行为不符; - 组件
<script setup>里配合storeToRefs解构,模板就能只写{{ count }}而不是{{ store.count }}。
2. $subscribe:监听“state 变了”
const stop = store.$subscribe((mutation, state) => {
console.log(mutation.type); // MutationType:'direct' | 'patch object' | 'patch function'
console.log(mutation.storeId); // 哪个 store
console.log('新 state', state);
});
// 组件卸载时别忘了:stop() (或用 detached: true 让它不随组件销毁)
实测发现 v4 的三个点:
- 默认回调时机不是同步——改完 state 后同一 tick 内不触发,要等下一拍(Vue 的 flush 队列)才回调:
需要“改完立刻拿到通知”就传第二参[实测] 同一 tick 内:default= [] sync= ["sync:1"]一 tick 后 :default= ["def:1"] sync= ["sync:1"]
{ flush: 'sync' }。 mutation.type能区分三种改法(直接赋值 /$patch对象 /$patch函数):(对应[实测] MutationType 序列:direct赋值 → patch(对象) → patch(函数)MutationType.direct、MutationType.patchObject、MutationType.patchFunction。)detached: true:store 被创建的组件卸载后订阅仍生效($subscribe默认绑定在创建它的组件作用域,detached帮你“断开关联”)。适合全局持久化这类长活订阅。
⚠️ $subscribe 只监听 state;getters/actions 变了它不知道。要听“有人调了 action”用下面这个。
3. $onAction:监听“有人要调 action”
const off = store.$onAction((ctx) => {
const { name, args, store, after, onError } = ctx;
console.log(`开始 action: ${name}(${args})`);
after((result) => console.log(`成功 ${name} 返回`, result)); // 成功钩子
onError((err) => console.log(`失败 ${name}`, err)); // 失败钩子
});
// 用 $onAction 可以对“动作”做审计/埋点/防抖
一个相关 v4 技巧(接篇 2的 helpers):setup store 里包在 action 外的内部函数默认不会被 $onAction 追踪,用 helpers.action(fn) 包一层即可:
const useW = defineStore('w', ({ action }) => {
const v = ref(0);
const inner = action((n) => { v.value = n; }, 'innerNamed'); // 命名可被 $onAction 看见
return { v, go: () => inner(42) };
});
[实测] w.go() 后 $onAction 捕获名:go, innerNamed → v= 42
4. 插件:给“每一个 store”加能力
插件 = pinia.use(函数)。函数在每个 store 创建时执行一次,拿到的上下文里有:pinia(当前实例)、app、store(这个 store)、options(这个 store 的定义元数据——这是插件和“store 作者”约定的暗号)。可以往 store 上挂属性、给特定 store 附加逻辑。
v4 重要:pinia.use(...) 注册后要等 app.use(pinia)(install)才真正执行插件——v4 会先把它暂存进队列。实测里用 createApp({}).use(pinia) 触发:
import { createApp } from 'vue';
import { createPinia, setActivePinia, defineStore } from 'pinia';
const storage = new Map(); // 模拟 localStorage
const pinia = createPinia();
pinia.use(({ store, options }) => { // ★ 插件体
store.$storage = storage; // 给所有 store 注入能力
store.hello = () => 'hi';
const persistKeys = options.data?.persist ?? []; // 读 store 作者声明的“要持久化”键
for (const k of persistKeys) {
const saved = storage.get(`${store.$id}.${k}`);
if (saved !== undefined) store.$state[k] = saved; // 水合:先读回
store.$subscribe(() => storage.set(`${store.$id}.${k}`, store.$state[k])); // 每次变更落盘
}
});
createApp({}).use(pinia); // ★ install 触发插件
setActivePinia(pinia);
const usePrefs = defineStore('prefs', {
data: { persist: ['theme'] }, // 自定义 options 字段:声明持久化
state: () => ({ theme: 'light', lang: 'zh' }),
actions: { toggle() { this.theme = this.theme === 'light' ? 'dark' : 'light'; } },
});
const p = usePrefs();
p.toggle();
await new Promise((r) => setTimeout(r, 0)); // $subscribe 默认非 sync,等一拍
console.log('theme=', p.theme, '已持久化键=', [...storage.keys()].join(','), '值=', storage.get('prefs.theme'));
[实测] 输出(真实运行):
theme= dark 已持久化键= prefs.theme 值= dark
插件做的事就三件:注入属性 / 读 options 约定 / 挂订阅。这里 options.data.persist 是 store 作者自定义的字段——plugin 与 store 之间靠这种“软约定”通信。生产里持久化不用自己写,直接用社区包 pinia-plugin-persistedstate(本片这个是为了让你看懂它干了啥)。
TS 扩展插件挂的属性
在 .d.ts 里声明,store.$storage / store.hello 才有类型:
import 'pinia';
declare module 'pinia' {
export interface PiniaCustomProperties<Id, S, G, A> {
$storage: Map<string, unknown>;
hello: () => string;
}
}
5. 各自用在哪
| API | 用途 | 典型场景 |
|---|---|---|
storeToRefs | 读:解构保持响应 | 组件 <script setup> 取 state/getters |
$subscribe | 听 state:写了才通知 | 持久化、同步到后端、埋点 |
$onAction | 听 action:要调就通知 | 埋点/审计、loading、参数脱敏 |
pinia.use() 插件 | 扩展所有 store | 注入 $api、全局持久化、自动错误上报 |
动手
- 把上面持久化插件里
flush改成同步,比较水合/落盘的时机差别; - 用
$onAction给 cart 的submit包一层“请求中禁用重复提交”; - 给插件加个“前缀”参数(比如只对 id 以
persist:开头的 store 生效)。
自测
storeToRefs和直接解构 /toRefs的区别?$subscribe默认什么时候回调?怎么让它同步?第二参还能传什么?MutationType有哪三种取值?对应什么改法?$onAction的回调上下文能拿到什么?成功/失败怎么挂钩子?- v4 插件为什么
pinia.use()之后还要app.use(pinia)才生效?
下一篇:进阶与工程实战。