跳到主要内容

创建日期: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.nstore.doublestore.inc()——ref 被自动解包。

2. 两种写法的对照

关注点options storesetup store
statestate: () => ({ … })const x = ref(…),return 出来
gettersgetters: { double: (s) => … }const double = computed(() => …)
actionsactions: { 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 里 importconst 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

读三点:

  1. dbl 依赖另一个 storecounter.n,counter 变它立刻变——跨 store 联动靠的就是 computed 的依赖追踪;
  2. setup store 里 ref/computed 进 $state,函数(action)不进storeToRefs 也只挑 ref/computed 属性,函数原样留在 store 上;
  3. 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 允许——但别在代码库混着来,选一种风格贯彻。

动手

  1. 上一篇的 cart 改写成 setup store,体会 closure 存“私有”临时量;
  2. 造两个 store:auth(user/token)与 profile,让 profile 的 getter 读 auth 的 user,验证跨 store 联动;
  3. 写一个每次调用都 setActivePinia(createPinia()) 的测试函数,验证 store 实例会被隔离。

自测

  1. setup store 里分别用什么包 state / getters / actions?为什么必须 return?
  2. setup store 与 options store 相比,this 能用吗?
  3. 一个 store 怎么读另一个 store?为什么不是 this.其他Store
  4. $state 里会出现 setup store 的普通函数吗?storeToRefs 会不会把函数也解构出来?
  5. 怎么在无 app 的环境用 store?同一个 id 的 store 是单例吗?

下一篇:解构·订阅·插件