跳到主要内容

创建日期: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 的三个点

  1. 默认回调时机不是同步——改完 state 后同一 tick 内不触发,要等下一拍(Vue 的 flush 队列)才回调:
    [实测] 同一 tick 内:default= [] sync= ["sync:1"]
    一 tick 后 :default= ["def:1"] sync= ["sync:1"]
    需要“改完立刻拿到通知”就传第二参 { flush: 'sync' }
  2. mutation.type 能区分三种改法(直接赋值 / $patch 对象 / $patch 函数):
    [实测] MutationType 序列:direct赋值 → patch(对象) → patch(函数)
    (对应 MutationType.directMutationType.patchObjectMutationType.patchFunction。)
  3. 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 技巧(接篇 2helpers):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(当前实例)、appstore(这个 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、全局持久化、自动错误上报

动手

  1. 把上面持久化插件里 flush 改成同步,比较水合/落盘的时机差别;
  2. $onAction 给 cart 的 submit 包一层“请求中禁用重复提交”;
  3. 给插件加个“前缀”参数(比如只对 id 以 persist: 开头的 store 生效)。

自测

  1. storeToRefs 和直接解构 / toRefs 的区别?
  2. $subscribe 默认什么时候回调?怎么让它同步?第二参还能传什么?
  3. MutationType 有哪三种取值?对应什么改法?
  4. $onAction 的回调上下文能拿到什么?成功/失败怎么挂钩子?
  5. v4 插件为什么 pinia.use() 之后还要 app.use(pinia) 才生效?

下一篇:进阶与工程实战