创建日期:2026-09-08 | 最近更新:2026-09-08 本机 pinia 4.0.3 + vue 3.5.42 实测;
[实测]输出真实。上一篇:state / getters / actions。
组合式(setup)store 与多 store 协作
一句话:options store 是“填表”,setup store 是“写函数”——用
ref/computed产 state 和 getters、普通函数产 actions。它把 Pinia 变成纯 Vue 响应式 API 的组合,更灵活、也更适合复杂逻辑。这一篇把 setup store 讲透,再讲 store 之间怎么协作。
1. 为什么有 setup store
options store 很好懂,但「填表」的表达力有限:想在 store 里复用函数、用 closure 存私有临时量、把逻辑拆成多个小函数组合……都别扭。setup store 让你回到写普通组合式函数的方式,Pinia 只负责把你的返回值“编译”成一个响应式 store:
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
// —— state:用 ref() ——
const n = ref(0);
// —— getters:用 computed() ——
const double = computed(() => n.value * 2);
// —— actions:普通函数(直接改 ref)——
function inc(by = 1) { n.value += by; }
return { n, double, inc }; // ★ 必须 return,才会暴露成 store 的属性
});
在模板里用起来和 options store 一模一样:store.n、store.double、store.inc()——ref 被自动解包。
2. 两种写法的对照
| 关注点 | options store | setup store |
|---|---|---|
| state | state: () => ({ … }) | const x = ref(…),return 出来 |
| getters | getters: { double: (s) => … } | const double = computed(() => …) |
| actions | actions: { inc() { … } }(this 指 store) | 普通函数 + 直接操作上面的 ref |
| 内部“私有”变量 | 只能塞进 state(会进 $state) | 不进 $state 的 closure 变量随便用 |
| 类型推导 | 好 | 更直接(就是函数返回值类型) |
| 心智 | 填表声明 | 写普通组合函数 |
setup store 最被低估的好处:那些“只想在 store 内部暂存、不想变成响应式状态暴露给全 app”的量,直接在函数作用域里声明即可——它们不会出现在 $state,更不会被打包成 store 属性。这在写「轮询定时器句柄」「请求取消 token」这类内部物时非常干净。
3. v4 的新签名:setup 可以收一个 helpers
pinia 4 里 setup 函数签名是 defineStore(id, (helpers) => { … })。helpers 目前只给一个 action(fn, name?):把 store 内部的普通函数包一层,让它在从 store 内部被调用时也能被 $onAction 追踪(进阶场景,如第三方库在 store 内部发起长任务时想留钩子)。普通场景你完全可以不接收这个参数,旧式 () => ({ … }) 照样可用。
4. 多 store 协作:跨 store 调用
setup store 里“天然”能拿到其它 store——直接在函数体内 useCounterStore() 就行(它们共享同一个 active pinia,返回的都是同一个实例,所以别的 store 一变这里就联动):
const useCounter = defineStore('counter', { state: () => ({ n: 1 }), actions: { inc() { this.n++; } } });
const useSession = defineStore('session', () => {
const counter = useCounter(); // ① 跨 store 拿实例
const nickname = ref('anon');
const dbl = computed(() => counter.n * 2); // ② 别的 store 变 → 这里自动重算
function setNick(v) { nickname.value = v; }
return { nickname, dbl, setNick };
});
options store 之间协作同理:action 里 import 后 const other = useOtherStore(); other.xxx()——不能用 this,因为 this 只指向自己。
一整套实测(counter 动、session 联动):
import { createPinia, setActivePinia, defineStore, storeToRefs } from 'pinia';
import { ref, computed } from 'vue';
setActivePinia(createPinia());
// …上面的 useCounter / useSession 定义…
const s = useSession(); const c = useCounter();
console.log('初始 nickname=', s.nickname, 'dbl=', s.dbl);
c.inc(); c.inc();
console.log('counter.inc×2 → session.dbl 自动=', s.dbl);
s.setNick('kate');
console.log('action setNick → nickname=', s.nickname);
const { nickname, dbl } = storeToRefs(s);
nickname.value = 'destructured';
console.log('storeToRefs 改解构 → store.nickname=', s.nickname);
console.log('$state 键=', Object.keys(s.$state).join(','), '| $state.nickname 类型=', typeof s.$state.nickname);
console.log('storeToRefs 不含 action(setNick 仍是函数)=', typeof s.setNick);
[实测] 输出(真实运行):
初始 nickname= anon dbl= 2
counter.inc×2 → session.dbl 自动= 6
action setNick → nickname= kate
storeToRefs 改解构 → store.nickname= destructured
$state 键= nickname | $state.nickname 类型= string
storeToRefs 不含 action(setNick 仍是函数)= function
读三点:
dbl依赖另一个 store的counter.n,counter 变它立刻变——跨 store 联动靠的就是 computed 的依赖追踪;- setup store 里 ref/computed 进
$state,函数(action)不进;storeToRefs也只挑 ref/computed 属性,函数原样留在 store 上; - v4 里
$state表层访问已解包成值(nickname是字符串不是{ value })——序列化/传给别处直接用即可;它的内层其实仍是 ref,别对$state整体重新赋值。
5. 脱离组件 / 动态创建 store
在 .js 工具模块 / 路由守卫 / 测试里用(无组件环境):
import { setActivePinia, createPinia } from 'pinia';
setActivePinia(createPinia()); // 没有 app 也有一份“活动 pinia”
const store = useCartStore(); // 此刻才真正创建(懒加载)
每个 store 在同一个 pinia 里是单例——同一个 id 的 useStore() 永远返回同一个实例。这带来两种实用技巧:
- 按 id 动态取:某些场景 id 运行时才知道(多租户/每个页面一个 store),可以
defineStore(id, …)后use();不同 id 互不影响:
const useDyn = (id) => { const def = defineStore(id, { state: () => ({ v: 0 }) }); return def(); };
const a = useDyn('a'); const b = useDyn('b');
console.log('a 与 b 是不同实例?', a !== b); // true,各改各的
- 测试隔离:每个测试
setActivePinia(createPinia())重新来一份干净状态,避免 store 单例串场。
6. 什么时候用哪种
- 简单数据(token、开关、偏好):options store 最省事;
- 复杂逻辑 / 想写普通函数复用 / 有“内部物”:setup store;
- 同一个 store 想两种都行:Pinia 允许——但别在代码库混着来,选一种风格贯彻。
动手
- 把上一篇的 cart 改写成 setup store,体会 closure 存“私有”临时量;
- 造两个 store:
auth(user/token)与profile,让 profile 的 getter 读 auth 的 user,验证跨 store 联动; - 写一个每次调用都
setActivePinia(createPinia())的测试函数,验证 store 实例会被隔离。
自测
- setup store 里分别用什么包 state / getters / actions?为什么必须 return?
- setup store 与 options store 相比,
this能用吗? - 一个 store 怎么读另一个 store?为什么不是
this.其他Store? $state里会出现 setup store 的普通函数吗?storeToRefs会不会把函数也解构出来?- 怎么在无 app 的环境用 store?同一个 id 的 store 是单例吗?
下一篇:解构·订阅·插件。