状态管理
什么是状态管理
在前端开发中,状态(State) 指的是应用中可变化的数据,比如用户信息、购物车列表、页面切换的标签页、表单输入值、全局主题配置等。
状态管理 则是对这些状态进行统一的创建、读取、修改、监听和共享 的一套规则和工具,核心目标是解决状态分散、流转混乱、难以维护的问题。
Vue 项目中,组件间共享数据的原生方式有哪些?
- 父子组件:
props向下传、emit向上触发事件; - 跨层级 / 兄弟组件:依赖
EventBus或父组件中转; - 全局数据:挂载到
Vue.prototype或window上。
这些在项目规模扩大后会暴露致命问题
- 数据在多个组件间层层传递(“prop”),溯源和调试困难状态流转混乱:
- 状态修改不可控:任何组件都能随意修改全局数据,出现 Bug 后无法定位修改源头
- 状态同步问题:多个组件依赖同一状态时,修改后无法自动同步更新
- 逻辑分散:与状态相关的业务逻辑(如登录、购物车计算)散落在各个组件中,代码复用性差
所以说,状态管理在相对大型的前端项目中是必须的
状态管理在前端的必要性
https://blog.csdn.net/2301_80216352/article/details/155459782
从一个具体的场景来理解为什么要使用状态管理的库。 假设我们正在开发一个电商网站,这个网站有很多不同的页面和组件:首页展示商品列表,商品详情页面显示具体信息,购物车页面显示用户选择的商品,还有顶部的导航栏显示购物车中商品的数量。
在传统的组件开发模式中,每个组件管理自己的数据。比如购物车组件有自己的商品列表数据,商品详情页面有自己的商品数据,导航栏组件需要显示购物车商品数量。问题来了:当用户在商品详情页面点击”加入购物车”按钮时,购物车页面和导航栏需要同时更新。如果每个组件都维护自己的数据副本,我们就需要在多个地方重复更新数据,这很容易导致数据不一致。
状态分散在各个组件中,当多个组件需要相同的数据时,数据同步变得复杂且容易出错。特别是当组件层级很深,或者组件之间没有直接的父子关系时,数据传递变得非常困难。
Pinia 提供的是一种集中式状态管理方案。想象一下,我们有一个中央仓库(store),所有的数据都集中存放在这里。任何组件需要数据时,都从这个中央仓库获取。当数据发生变化时,所有使用这个数据的组件都会自动更新。就像是一家公司的所有部门都从同一个数据中心获取信息,确保每个人看到的数据都是一致的。
那么什么情况下需要使用 Pinia 呢?当两个或两个以上的组件需要共享相同的数据时,就应该考虑使用 Pinia。比如用户登录信息、购物车数据、全局的主题设置等。但是,并不是所有的状态都需要放到 Pinia 中。比如一个弹窗组件是否显示,这种只影响单个组件 UI 的状态,完全可以在组件内部管理。区分的关键在于:这个状态是否被多个不相关的组件使用?是否需要跨页面持久化?如果是,那么应该使用 Pinia;如果只在一个组件内部使用,那么使用组件的本地状态就足够了。
认识Vue的状态管理库Pinia
Vue 官方曾主推 Vuex 作为状态管理库(Vue2 时代),但 Vue3
发布后,官方更推荐 Pinia(已成为 Vue
官方状态管理库),Pinia 解决了 Vuex 的诸多痛点,且更贴合 Vue3 的
Composition API。
Pinia(发音 /ˈpiːnjə/,西班牙语 “菠萝”)是 Vue 官方推荐的状态管理库,适用于 Vue2 和 Vue3,核心特点是:
- 抛弃 Vuex 的
Mutation概念,简化状态修改逻辑; - 天然支持 TypeScript,类型推断完善;
- 支持 Vue3 的 Composition API 和 Vue2 的 Options API;
- 无嵌套模块限制,以 “Store” 为核心,结构更扁平;
- 体积极小(约 1KB),性能优异;
- 完全兼容 Vue Devtools(支持时间旅行、状态快照)。
安装 Pinia
1 | npm install pinia |
创建 Pinia 实例并挂载到 Vue 应用
1 | // src/main.ts |
Pinia 本身不支持持久化,需配合插件
pinia-plugin-persistedstate:
安装:npm install pinia-plugin-persistedstate;
配置
1 | // src/main.ts |
在 Store 中启用持久化
1 | export const useCounterStore = defineStore('counter', { |
一般来说,状态管理的相关文件都会放到名为 store 的文件夹,这个文件夹将存放所有的 store 文件。
创建 src/stores/counter.ts
1 | import { defineStore } from 'pinia' |
在组件中使用 Store
1 | <!-- src/components/Counter.vue --> |
使用Pinia
定义和使用Store
Store是什么
Pinia 的核心是 Store(仓库),每个 Store 是一个独立的状态容器,可理解为 “全局组件”,包含:
| 概念 | 作用 | 类比 Vue 组件 |
|---|---|---|
state |
存储核心状态(唯一数据源) | data |
getters |
基于 state 计算派生的值(缓存) | computed |
actions |
修改状态的方法(支持同步 / 异步) | methods |
往详细了说,,Store(仓库) 是状态管理的核心载体,用于集中存储和管理应用中需要跨组件共享的数据、派生状态以及修改状态的方法。它可以理解为一个 “全局的响应式容器”,独立于组件之外,却能被任意组件访问和修改,从而实现组件间的状态共享和协同。
Store 的核心特性如下
- 独立性:每个 Store 通过唯一的
id标识(如wordbook、counter),彼此隔离,避免状态污染。 - 响应式:Store 中的状态基于 Vue 的响应式
API(
ref、reactive)实现,当状态变化时,依赖该状态的组件会自动更新。 - 可访问性:任何组件都可以引入并使用 Store,无需通过 “props 层层传递” 或 “事件冒泡” 等方式共享数据。
- 完整性:包含状态(
state)、计算属性(getters)和方法(actions),覆盖状态管理的完整需求。
其中
状态(State)
定义:使用 Vue 的响应式 API(
ref、reactive)声明,是 Store 的 “数据源”,存储原始数据。作用:保存应用中需要共享的核心数据(如单词列表、计数器数值)。
示例:
1
2
3
4
5// counter.ts 中的状态
const count = ref(0); // 计数器数值
// wordbook.ts 中的状态
const words = ref<Word[]>([...]); // 单词列表- 这里的
count和words都是 Store 的state,它们是响应式的,修改后会触发依赖更新。
- 这里的
Getters(计算属性)
定义:使用
computed()声明,基于state派生的状态。作用:对
state进行加工处理(如过滤、计算),并缓存结果(只有依赖的state变化时才会重新计算)。示例:
1
2
3
4
5
6// counter.ts 中的 getter
const doubleCount = computed(() => count.value * 2); // 基于 count 计算双倍值
// wordbook.ts 中的 getter
const easyWords = computed(() => words.value.filter(w => w.difficulty === 'easy')); // 过滤简单单词
const totalWords = computed(() => words.value.length); // 计算单词总数- 这里的
doubleCount、easyWords等都是getters,用于简化组件中对派生状态的访问。
- 这里的
Actions(方法)
定义:直接声明函数,用于修改
state或处理业务逻辑(支持同步和异步操作)。作用:集中管理状态的修改逻辑,避免在组件中直接操作
state导致的混乱,便于维护和调试。示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// counter.ts 中的 action
function increment() {
count.value++; // 修改 count 状态
}
// wordbook.ts 中的 action
function addWord(word: Omit<Word, 'id' | 'createdAt'>) {
const newWord: Word = { ...word, id: Date.now(), createdAt: new Date() };
words.value.push(newWord); // 修改 words 状态
}
function removeWord(id: number) {
const index = words.value.findIndex(w => w.id === id);
if (index > -1) words.value.splice(index, 1); // 删除指定单词
}这些方法都是
actions,组件通过调用它们来间接修改state,确保状态变化可追踪
Pinia 的 Store 是一个 “自包含” 的状态管理单元,通过
state 存储数据、getters
处理派生状态、actions
管理状态修改,实现了状态的集中化、响应式和可维护性。
相比传统的 Vuex,Pinia 的 Store 更简洁(无需区分
mutations 和 actions)、类型支持更好,且与
Vue3 的组合式 API 无缝衔接
定义Store
Pinia 使用 defineStore() 函数创建
store,它的第一个参数要求是一个独一无二的名字,基本结构如下:
1 | import { defineStore } from 'pinia' |
这个名字 ,也被用作 id ,是必须传入的, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use… 是一个符合组合式函数风格的约定。
使用Vue组合式api定义Store,类似setup函数,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象,例如
1 | import { ref, computed } from 'vue' |
在 Setup Store 中:
ref()就是state属性computed()就是gettersfunction()就是actions
注意,要让 pinia 正确识别
state,你必须在 setup store 中返回
state 的所有属性。这意味着,你不能在 store
中使用私有属性。不完整返回会影响 SSR
,开发工具和其他插件的正常运行。
这是什么意思
必须完整返回所有响应式状态(state),不能有未暴露的
“私有” 状态。在 Setup Store 中,用
ref()/reactive() 定义的响应式变量就是 Store 的
state。这些状态需要被 Pinia 内部机制识别和管理。
“私有属性” 指的是在 Store 内部定义但没有通过 return
暴露的变量。例如:
1 | export const useCounterStore = defineStore('counter', () => { |
使用Store
虽然我们前面定义了一个 store,但在我们使用
<script setup> 调用 useStore()(或者使用
setup() 函数,像所有的组件那样)
之前,store 实例是不会被创建的
也就是说,Store 的实例化需要调用 useStore()
1 | <script setup> |
因为本质上, 我们前面定义的useCounterStore 本质是一个
“创建 Store 的函数”,不是 Store 本身;只有按下启动键(调用
useCounterStore()),才会做出菠萝(Store
实例);未调用前,Store 实例不存在,也就无法访问里面的
count/increment 等属性。但是一旦 store
被实例化,你可以直接访问在 store 的
state、getters 和 actions
中定义的任何属性。
而且
- 每个组件调用
useCounterStore(),拿到的是同一个实例(单例),这也是 “全局状态” 的核心 —— 所有组件共享同一个 Store; - 官方文档会提示 “不同文件定义不同 Store”,这是很有必要的:比如把用户
Store 放
user.ts、购物车放cart.ts,好处是代码可拆分、TS 类型推断更准、打包时能按需加载。
这也就是为什么,我们定义 Store 的函数名,基本都会用 use 打头
而且注意,Store 是 reactive
包装的对象:直接解构会丢响应式
1 | <script setup> |
前面我们说过,Vue 的 reactive 有个限制,对
reactive 对象直接解构,会把属性从 “响应式对象”
中剥离出来,变成普通值(非响应式)。而Pinia 的 Store
底层就是用 reactive 包装的
正确解构我们通常使用 storeToRefs() 保留响应式
1 | <script setup> |
storeToRefs() 是 Pinia 专门解决 “解构丢响应式”
的工具,它的作用是:
- 遍历 Store 中的属性,把响应式状态(state/getters)
转换成
ref类型(比如store.count→ref(0));- 只有需要把状态赋值给变量时,才需要
storeToRefs,不要处处用
- 只有需要把状态赋值给变量时,才需要
- 跳过
action(因为 action 是函数,本身不需要响应式); - 这样解构出来的
count是ref对象,修改store.count时,count.value会同步更新。- 解构后别忘记
.value,storeToRefs解构的是ref类型,在<script>中使用需要加.value,模板中不用
- 解构后别忘记
为什么 action 可直接解构?
action 是 Store 上的 “方法”(比如
increment),方法的指向是固定的,不管怎么解构,调用
increment() 本质还是调用
store.increment(),所以不需要特殊处理。
而且你解构 action
会报错,const { increment } = storeToRefs(store) →
increment 是函数,不是 ref/reactive
1 | <script setup> |
总结,使用 Store 需要注意这些内容
- Store 是 “懒实例化” 的:调用
useXxxStore()才会创建实例,不是定义了就存在; - Store 是
reactive包装的对象:直接解构会丢失响应式; - 解构响应式状态用
storeToRefs(),action 可直接解构。
那么,完整的使用 store 的例子如下
1 | <!-- src/components/CounterDemo.vue --> |
State
理解 State,及其适配TypeScript
前面说了,State 是 Store 的唯一数据源,承载了 Store 的基础响应式数据
Pinia 的 State 本质是:用 Vue 响应式
API(ref/reactive)定义的、存储 Store
基础数据的容器,是 Store 中所有数据的 “源头”(Getters 基于
State 派生,Actions 用于修改 State)
别忘了 Pinia 底层复用了 Vue 的响应式系统
- 在
Setup Store(组合式 API 写法)中,ref()定义的是单个响应式状态,reactive()定义的是对象型响应式状态; - 在
Options Store(选项式写法)中,state函数返回的对象会被 Pinia 自动包装成reactive对象; - 因此 State 的响应式行为和 Vue 组件内的
data/ref/reactive完全一致,遵循 Vue 响应式规则(如对象属性新增需用reactive或ref、数组修改需用原生方法等)。
如果希望适配 TypeScript,需要开TS 的 strict
模式(严格模式),因为它包含了 noImplicitThis(禁止隐式的
this 类型)等规则,开启后 TS 能更精准地分析代码类型。
Pinia 依赖 TS 的类型推导能力,开启这些规则后,大部分简单场景下,Pinia 能自动识别 State 的类型,无需手动写类型注解。
1 | const useStore = defineStore('storeId', { |
此时你在组件中访问 store.count,TS 会提示它是
number 类型,赋值非数字会报错,这就是 “自动推断”
的好处。
但是,在某些情况下,应该帮助它进行一些转换:
初始化空列表
userList: []如果直接写
userList: [],TS 会默认推断为never[](空数组,不允许添加任何元素),这显然不符合我们的预期(我们希望它是UserInfo[]类型的数组)。所以需要手动标注类型:
1
userList: [] as UserInfo[], // 告诉 TS:这个空数组是 UserInfo 类型的数组
尚未加载的数据(
user: null)如果直接写
user: null,TS 会推断为null类型(只能是 null,不能赋值为 UserInfo 对象),但我们的实际需求是:user要么是null(未加载),要么是UserInfo对象(加载完成)。所以需要手动标注联合类型:
1
user: null as UserInfo | null, // 告诉 TS:user 可以是 null 或 UserInfo 对象
更规范的写法就是用接口定义整个 State 类型
1 | // 定义用户信息类型 |
什么意思?说这么多是在说?
- Pinia 对 TS 友好:开启严格模式后,简单场景能自动推断 State 类型;
- 别忘了采用TS的规范写法:用接口定义整个 State 结构,让类型更清晰、易维护;
编写和访问 state
Pinia 支持两种 Store 写法,对应两种 State 定义方式,基本都是 “返回初始值 + 保证响应式”。
Setup Store(Vue3 组合式 API 风格)
最常用的写法,直接用 Vue 的
ref/reactive定义 State,无需包裹在state函数中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
export const useUserStore = defineStore('user', () => {
// 单个值状态(ref 推荐,解构更友好)
const userId = ref<string>('1001')
const userName = ref<string>('张三')
// 对象/数组状态(reactive 或 ref 都可)
// 推荐:简单对象用 reactive,复杂对象/需解构用 ref
const userInfo = reactive({
age: 20,
phone: '13800138000'
})
const roles = ref<string[]>(['admin', 'user'])
// 必须返回所有 State(否则 Pinia 无法追踪,DevTools/SSR 异常)
return { userId, userName, userInfo, roles }
})Options Store(Vue2 选项式风格,兼容 Vue2/Vue3)
模仿 Vuex 的写法,通过
state函数返回初始值(必须是函数,避免多个 Store 实例共享同一引用):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
// 必须是函数:每次实例化 Store 都会执行,返回新的状态对象
state: () => ({
userId: '1001',
userName: '张三',
userInfo: {
age: 20,
phone: '13800138000'
},
roles: ['admin', 'user']
}),
getters: { /* ... */ },
actions: { /* ... */ }
})只提到一下,不会细说
在写 State 部分的时候,注意
- 必须用函数返回初始值(Setup Store 中
ref/reactive本身是函数式创建,Options Store 中必须写state: () => ({})):避免多个组件共享同一个状态对象的引用,导致状态污染; - 类型标注:推荐用 TypeScript 显式标注 State 类型(如
ref<string>('1001')),Pinia 会自动推导,但显式标注更清晰;这个下面会细说 - 不要定义私有 State:所有 State 必须通过
return暴露(Setup Store),否则 Pinia 无法追踪,DevTools 看不到、SSR 会丢失数据。
访问 state 相对来说没有那么多事
默认情况下,你可以通过 store 实例访问
state,const store = useStore(),直接对其进行读写。
1 | const store = useStore() |
注意,新的属性如果没有在 state()
中被定义,则不能被添加,它必须包含初始状态。
例如:如果 secondCount 没有在 state()
中定义,我们无法执行 store.secondCount = 2。
重置 state
有时候我们修改了state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。
你可以通过调用 store 的 $reset() 方法将 state
重置为初始值。将当前 Store 的 state
覆盖为初始化时的原始状态副本。
1 | const store = useStore() |
Pinia 在创建 Store 时,会先缓存一份初始 state(比如定义
state() 函数返回的对象),调用 $reset()
时,会把这份缓存的初始状态深拷贝(或浅拷贝,取决于数据类型)覆盖到当前
state 上,从而实现 重置。
用户填写表单时,可能需要 “清空表单 / 恢复默认值”,比如新增表单的 “取消” 按钮:
1 | <template> |
如果 Store 中有多个关联状态,需要一次性还原时,$reset()
比手动逐个赋值更高效:
1 | // 手动重置(繁琐,易遗漏) |
$reset() 仅重置 state,不会改变
actions、getters 的定义,也不会触发
actions(仅修改状态)。
如果使用 Setup
Store(而非选项式),$reset()
不会自动生成,需要手动实现:
1 | // stores/setupStore.js |
注意,$reset()核心是
“还原到初始状态”,适用于全量重置场景,如果不需要重置全部
state,仅需重置部分状态,用手动赋值
变更 state
state 本质上是个响应式对象,基于 Vue 的
reactive/ref 实现,因此变更 state
的核心原则是保持响应式的前提下修改状态。
官方提供了三类变更 state 的方式
直接修改(简单场景);
直接通过 Store 实例访问并修改
state属性,Pinia 会基于 Vue 的响应式系统自动追踪变更。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 定义 Store(选项式)
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Vue',
items: [{ id: 1, text: 'test' }]
})
})
// 组件中使用
const counterStore = useCounterStore()
// 直接修改单个属性
counterStore.count++ // 数字自增
counterStore.name = 'Pinia' // 替换字符串
// 直接修改嵌套属性(响应式仍生效)
counterStore.items[0].text = 'updated'$patch补丁对象(多属性批量修改);向
$patch传入一个补丁对象(键为 state 属性名,值为新值),Pinia 会一次性合并所有变更,并将整个操作归为 devtools 中的一条记录。1
2
3
4
5
6
7// 批量修改多个属性
counterStore.$patch({
count: counterStore.count + 2, // 基于旧值修改
name: 'Vue + Pinia',
// 注意:数组/对象直接替换才会生效(响应式要求)
items: [...counterStore.items, { id: 2, text: 'new item' }]
})但是,
$patch对集合(数组 / 对象)的限制:仅支持 “替换式修改”,无法直接修改集合的内部元素1
2
3
4
5
6
7
8
9
10
11// ❌ 无效:补丁对象中直接修改数组元素,Pinia 无法追踪
counterStore.$patch({
items[0].text: 'error' // 语法错误,且响应式失效
})
// ✅ 正确:先创建新数组,再替换原数组
counterStore.$patch({
items: counterStore.items.map(item =>
item.id === 1 ? { ...item, text: 'updated' } : item
)
})即使修改多个属性,
$patch也只会触发一次响应式更新(相比多次直接修改更高效),且 devtools 中仅显示一条 “patch” 记录$patch函数(复杂操作,如数组 / 对象嵌套修改)。向
$patch传入一个修改函数,函数接收state作为参数(直接操作原始 state),Pinia 会将函数内的所有操作归为 devtools 中的一条记录1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 1. 数组复杂操作
counterStore.$patch((state) => {
state.items.push({ id: 3, text: 'push item' }) // 新增元素
state.items.splice(0, 1) // 删除第一个元素
state.items = state.items.filter(item => item.id > 1) // 过滤数组
})
// 2. 嵌套对象修改
counterStore.$patch((state) => {
state.user = state.user || {} // 兜底判断
state.user.info = state.user.info || {}
state.user.info.age = 20 // 深层修改
})
// 3. 带逻辑的批量修改
counterStore.$patch((state) => {
if (state.count > 10) {
state.count = 0
state.name = 'reset'
} else {
state.count += 5
}
})
如果使用 Setup
Store(组合式写法),变更逻辑完全一致,仅需注意
state 是通过 ref/reactive
定义的响应式数据
1 | // Setup Store 定义 |
注意,避免频繁修改 state,可将高频修改逻辑封装到
actions 中(便于复用和测试),而非直接在组件中修改;
替换 state
你不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是,你可以 patch 它。
1 | // 这实际上并没有替换`$state` |
你也可以通过变更 pinia 实例的 state
来设置整个应用的初始 state。
1 | pinia.state.value = {} |
订阅 state
Pinia 的 $subscribe() 是专门用于侦听 Store 中
state 变更的 API,其设计目标是替代普通的 watch()
实现更高效、更贴合状态管理的侦听逻辑。
1 | const store = useStore() // 获取 Store 实例 |
回调函数接收两个核心参数:mutation(变更元信息)和
state(变更后的最新 state)。
| 参数 | 类型 | 说明 |
|---|---|---|
mutation |
Object | 变更的元数据,包含 3 个核心属性:-
type:变更类型(direct/patch object/patch function)-
storeId:当前 Store 的唯一 ID(如 cart)-
payload:仅 patch object 类型时可用,即传入
$patch() 的补丁对象 |
state |
Object | 变更后的完整 state(响应式对象) |
例如:监听购物车 state 并且持久化
1 | // stores/cart.js |
$subscribe() 的第二个参数是配置对象,继承自 Vue 的
watch
配置(flush,deep,immediate),但是新增了
detached 专属配置:
detached:是否与组件分离
false(默认):订阅器与当前组件绑定,组件卸载时自动取消订阅(避免内存泄漏);true:订阅器与组件分离,组件卸载后仍保留(适合全局监听,如全局状态持久化)。
如果需要监听所有 Store 的 state 变更(而非单个
Store),可直接监听 Pinia 实例的 state 属性:
1 | import { createPinia } from 'pinia' |
也可以取消订阅,手动取消使用返回值,$subscribe()
会返回一个取消订阅的函数,调用即可停止监听:
1 | const unsubscribe = cartStore.$subscribe((mutation, state) => { |
如果在组件的 setup() 中调用 $subscribe()
且未设置
detached: true,组件卸载时会自动取消订阅,无需手动处理:
1 | <script setup> |
Setup Store 中使用 $subscribe() 与选项式 Store
完全一致,无需额外配置
Getter
理解 Getter
Getter 完全等同于 store 的 state 的计算值。可以通过
defineStore() 中的 getters
属性来定义它们。推荐使用箭头函数,并且它将接收
state 作为第一个参数
这段话是官网上对 Getter 的准确描述,所以它是什么意思
可以把Getter和 Vue 组件里的computed(计算属性) 做直接类比
- Vue
组件的
computed是基于组件的data/props进行派生计算,有缓存特性,依赖变化时才重新计算。 - Pinia 的
Getter是基于 store 的state进行派生计算,同样有缓存特性,依赖的 state 变化时才重新计算
所以官网说 “Getter 完全等同于 store 的 state 的计算值”,本质就是:Getter 是针对 store 的 state 的 “计算属性”,作用和组件的 computed 一致,只是作用域在 store 中。而且大多数时候,getter 仅依赖 state。
而且 Getter 本质是 “计算属性”,不是函数,所以不能直接传参。
使用Getter
首先,在defineStore中通过getters属性定义,一般用箭头函数(因为箭头函数不绑定
this,能更清晰地接收 state 参数),并且它将接收 state
作为第一个参数:
1 | export const useCounterStore = defineStore('counter', { |
那么,Getter 还支持很多种的定义形式
1 | <script setup> |
这是 Getter 的使用方式:定义好的 Getter 会挂载到 store 实例上,像访问普通属性一样直接使用(不需要加括号调用,因为它是计算属性,不是函数)。
1 | // 在组件中使用 |
这里要注意:Getter
是计算属性,不是方法,所以直接写store.doubleCount,而不是store.doubleCount()。
而且,想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:
1 | import { useOtherStore } from './other-store' |
Getter 如何访问其他 Getter
与计算属性一样,你也可以组合多个 getter。
除了通过state参数,还能通过this(常规函数)访问整个
store 实例,包括其他 Getter、state 甚至 actions。
“大多数时候,getter 仅依赖 state。不过,有时它们也可能会使用其他 getter。”
也就是,Getter
不仅能基于state计算,还能基于其他已定义的
Getter做二次计算。
比如:
- 先定义
doubleCount(基于 state.count 计算) - 再定义
doublePlusOne(基于doubleCount再 + 1)
这时候就需要一种方式让doublePlusOne访问doubleCount,有两种实现方式:
用
state参数访问其他 Getter,这是很好的方式,且不会引发类型问题,而且无需关心 TypeScript 类型问题,返回类型会自动推断。1
2
3
4
5
6
7
8
9getters: {
doubleCount(state) {
return state.count * 2;
},
// 用state参数访问其他Getter(state会包含所有getter)
doublePlusOne(state) {
return state.doubleCount + 1;
}
}用
this访问整个 store 实例,如果用常规函数定义 Getter,this会指向整个 store 实例,因此可以通过this.doubleCount访问其他 Getter,也能通过this.count访问 state:1
2
3
4
5
6
7
8
9
10getters: {
doubleCount(state) {
return state.count * 2;
},
// 常规函数,this指向store实例
doublePlusOne() {
// this可以访问:this.count(state)、this.doubleCount(其他getter)、甚至this.xxxAction(actions)
return this.doubleCount + 1;
}
}但这种方式在TypeScript 中会有问题,这就引出了下一个规则。
TypeScript 下的类型标注问题
使用this时必须显式定义返回类型,这是为了规避 TypeScript
的类型推断缺陷;而用state参数(箭头函数 /
常规函数)则不需要。
当 Getter 通过this访问其他 Getter 时,TypeScript
会陷入循环类型推断(比如doublePlusOne依赖doubleCount,doubleCount又属于
store 的一部分,store
又包含doublePlusOne),导致无法自动识别返回类型,甚至出现类型报错。
在 TypeScript 中,只要 Getter
里用了this,就必须手动写返回类型(比如: number),而不用this则不需要。
1 | import { defineStore } from 'pinia'; |
“不过这不影响用箭头函数定义的 getter,也不会影响不使用 this 的 getter。”
因为箭头函数没有自己的this(this会指向外层作用域,而非
store
实例),所以只能用state参数,自然不会涉及this的类型问题,TypeScript
能正常推断返回类型。
即使是常规函数,只要只用到state参数、不用this,TypeScript
也能自动推断返回类型。
向 getter 传递参数
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
1 | export const useUserListStore = defineStore('userList', { |
并在组件中使用:
1 | <script setup> |
请注意,当你这样做时,getter 将不再被缓存。它们只是一个被你调用的函数。
不过,可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好
1 | export const useUserListStore = defineStore('userList', { |
Action
Action 相当于组件中的 method
- 组件的
method是用于定义组件的业务逻辑(比如点击事件处理、数据请求、复杂操作),可以是同步 / 异步的。 - 那么 Pinia 的
actions就是用于定义store 的业务逻辑(比如修改 state、异步请求数据、调用其他 actions),同样支持同步 / 异步,是处理 store 中动态逻辑的核心。
state是 store 的 “数据”,getters是 store 的
“计算属性”,actions是 store 的 “方法”。
使用action
基本定义:通过actions属性定义,用this访问
store 实例
和
getters类似,actions中可以通过this访问整个 store 实例,包括:this.xxx:访问state中的数据(如this.count)。this.xxxGetter:访问getters中的计算属性(如this.doubleCount)。this.xxxAction():调用其他actions方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import { defineStore } from 'pinia'
export const useCounterStore = defineStore('main', {
state: () => ({
count: 0, // 原始数据
}),
getters: {
doubleCount: (state) => state.count * 2, // 计算属性
},
actions: {
// 同步action:修改state
increment() {
this.count++ // this指向store实例,直接修改state
},
// 带参数的action:更灵活地修改state
incrementBy(num) {
this.count += num
},
// 调用其他action + 访问getters
incrementAndDouble() {
this.increment() // 调用当前store的其他action
console.log('当前doubleCount:', this.doubleCount) // 访问getters
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
})- Actions不推荐用箭头函数定义:因为箭头函数没有自己的
this,无法指向 store 实例,会导致this.count等操作报错。 - Actions 修改 state 是直接赋值
Action 可以是异步的
那么使用很简单,Actions 的调用方式像调用普通方法一样
定义好的 Actions 会挂载到 store 实例上,调用方式和普通函数 / 方法完全一致,支持:
- 组件的
<script setup>中调用。 - 模板中直接调用(比如绑定点击事件)。
- 其他 store 的 Actions 中调用。
1 | <script setup> |
访问其他 store 的 action 也很简单,想要使用另一个 store 的话,那你直接在 action 中调用就好了:
1 | import { useAuthStore } from './auth-store' |
订阅 action
Pinia 的订阅 Action(subscribeAction) 是一种监听机制,允许你在Action 被调用前、调用后、抛出错误时,捕获到 Action 的执行信息(比如 Action 名称、参数、返回值、错误信息等),并执行自定义逻辑。
给 store 的所有 Action 装了一个 “监听器”,无论哪个 Action 被触发,这个监听器都能收到通知,并做相应处理。
这样就无需在每个 Action 内部重复写相同的逻辑
通过 store.$onAction() 来监听 action
和它们的结果。传递给它的回调函数会在 action
本身之前执行。,并且可以通过内置的after和onError钩子,分别监听
Action执行成功后和执行出错后的状态
$onAction接收两个参数:
- 第一个参数:回调函数,参数是一个包含 Action
上下文和钩子的对象(
name/store/args/after/onError)。 - 第二个参数:布尔值(可选),默认
false(订阅器绑定当前组件,组件卸载时自动删除);设为true时,订阅器与组件解耦,组件卸载后仍保留。
$onAction执行后会返回一个取消订阅的函数,可手动调用删除监听器。
1 | // 调用store的$onAction方法,传入回调函数,得到取消订阅的函数 |
来说一下文档中提到的$onAction的组件绑定特性
- 默认行为(第二个参数为
false)- 如果在组件的
setup()中调用store.$onAction,订阅器会绑定到当前组件。 - 当组件被卸载时,订阅器会自动删除,无需手动调用
unsubscribe,避免内存泄漏。
- 如果在组件的
- 解耦行为(第二个参数为
true)- 订阅器与当前组件解耦,组件卸载后,订阅器仍然生效。
- 适用场景:全局级别的 Action 监听(如全局日志、埋点),需要在整个应用生命周期内保留。
- 注意:这种情况需要手动调用
unsubscribe来删除订阅器,否则会导致内存泄漏。
Vue 与 Vite 的 SSR
官方文档首先强调:只要你在setup函数、getter、action
的顶部调用useStore(),Pinia 的 SSR
支持就是开箱即用的。
setup内部:useStore()无需传参,Pinia 自动处理上下文。setup外部:useStore(pinia)必须传 pinia 实例,否则会报错。
这是因为 Pinia 在 SSR 环境中会自动管理store 的实例作用域—— 它能识别当前运行的是服务端还是客户端,以及对应的应用实例,从而确保每个请求 / 组件都能获取到正确的 store 实例
1 | <!-- 组件中:setup函数顶部调用useStore,SSR正常工作 --> |
在setup函数、getter、action
的顶部调用useStore()时,Pinia 能通过 Vue 的 SSR
上下文,自动关联到当前应用的 pinia 实例,不会出现 “找不到 pinia 实例” 或
“状态共享污染” 的问题。
但是,如果在setup外部使用 store,必须手动传 pinia
实例
例如路由守卫,serverPrefetch、工具函数等setup外部的场景使用
store,直接调用useStore()会报错(因为 Pinia
无法自动识别上下文),此时必须将应用的 pinia
实例作为参数传递给useStore()。
1 | // main.ts(服务端/客户端共用的入口文件) |
SSR 的核心流程是:服务端渲染页面时,先获取数据并更新 store 的 state → 将 state 序列化后嵌入到 HTML 中 → 客户端加载页面时,先恢复 store 的 state,再挂载应用(避免客户端重新请求数据,导致页面闪烁)。
这个 “服务端序列化 state,客户端恢复 state” 的过程,Pinia 称之为State 激活(State Hydration)。
State 激活的步骤
注意,代码只是示例理解 Pinia 在 SSR 中是如何工作的,绝大多数场景下,不需要手写这些代码,现代 Vue SSR 生态已经把这些底层逻辑封装好了
服务端:渲染后获取并序列化 pinia 的 state
服务端渲染页面时,完成所有数据预取后,pinia 的根状态会存储在
pinia.state.value中。需要将其序列化并转义(防止 XSS 攻击),然后嵌入到 HTML 的全局变量中。官方推荐使用
@nuxt/devalue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49// 服务端渲染入口文件(如server/render.ts)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import devalue from '@nuxt/devalue'
import { renderToString } from 'vue/server-renderer'
import App from './App.vue'
import { router } from './main'
export async function render(url: string) {
// 1. 每个请求创建新的pinia和app实例(避免跨请求状态污染)
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.use(router)
// 2. 路由跳转至当前请求的URL
await router.push(url)
await router.isReady()
// 3. 渲染应用为字符串(服务端渲染核心)
const appHtml = await renderToString(app)
// 4. 获取pinia的根状态(所有store的state)
const piniaState = pinia.state.value
// 5. 序列化并转义state(关键:防止XSS,@nuxt/devalue比JSON.stringify更安全)
const serializedState = devalue(piniaState)
// 6. 将序列化后的state嵌入到HTML中(作为全局变量)
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Pinia SSR</title>
</head>
<body>
<div id="app">${appHtml}</div>
<!-- 挂载序列化后的state到window.__INITIAL_PINIA_STATE -->
<script>
window.__INITIAL_PINIA_STATE__ = ${serializedState}
</script>
<!-- 客户端入口脚本 -->
<script type="module" src="/src/client.ts"></script>
</body>
</html>
`
return html
}客户端:恢复 pinia 的 state(挂载应用前)
客户端加载页面时,需要先从全局变量中读取序列化的 state,赋值给
pinia.state.value,再挂载应用。必须在调用任何
useStore()之前完成这一步,否则 store 会使用默认的空状态,导致数据不一致。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 客户端入口文件(如src/client.ts)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import { router } from './main'
// 1. 创建pinia实例
const pinia = createPinia()
// 2. 恢复pinia的state(关键:在挂载应用前完成)
if (typeof window !== 'undefined') {
// 从全局变量中读取序列化的state并赋值
pinia.state.value = window.__INITIAL_PINIA_STATE__
}
// 3. 创建并挂载应用
const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')
对了,如果你的项目是基于Nuxt(Vue 的 SSR 框架),不需要手动处理上述步骤,因为 Nuxt 与 Pinia 深度集成,会自动处理:
- store 的实例作用域(每个请求一个 pinia 实例)。
- state 的序列化与恢复。
useStore()在任意位置的调用(Nuxt 会自动传递 pinia 实例)。
只需按照 Nuxt 的文档配置 Pinia 即可,无需手动写 SSR 相关的代码。





