pinia状态管理工具
Pinia 是 Vue.js 官方推荐的新一代状态管理库,可以看作是 Vuex 的替代品。
1. 什么是 Pinia?
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。由 Vue.js 核心团队维护,并且对 TypeScript 有着极其出色的支持。
核心设计理念:
- 直观: 像定义组件一样定义 Store,API 设计尽可能简单直观。
- 类型安全: 全程提供出色的 TypeScript 支持,无需复杂的包装器。
- 模块化: 可以拥有多个 Store,并自然地编码,让 Store 自动打包。
- 轻量: 体积非常小(约 1KB),几乎不会增加打包负担。
- Composition API: 完美契合 Vue 3 的 Composition API,但同时也支持 Option API。
2. 核心概念(与 Vuex 对比)
Pinia 的核心概念比 Vuex 更加简化,去掉了容易令人混淆的 mutations
。
概念 | Vuex | Pinia | 说明 |
---|---|---|---|
状态 | state |
state |
存储应用状态数据的地方 |
获取状态 | getters |
getters |
用于计算和派生状态,相当于 Store 的计算属性 |
修改状态 | mutations |
actions |
这是最大的不同 。Pinia 中 actions 既可用于同步,也可用于异步操作 |
异步操作 | actions |
actions |
在 Pinia 中,异步和同步操作都在 actions 中完成 |
简单来说,Pinia 就是:state
+ getters
+ actions
。
3. 安装与设置
1. 安装:
bash
npm install pinia
# 或
yarn add pinia
2. 在 Vue 应用中创建并引入 Pinia:
在 main.js
或 main.ts
中:
javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 导入 createPinia 函数
import App from './App.vue'
// 创建 Pinia 实例
const pinia = createPinia()
// 创建 Vue 应用实例
const app = createApp(App)
// 使用 Pinia
app.use(pinia)
app.mount('#app')
4. 定义一个 Store
Pinia 使用 defineStore()
函数来定义一个 Store,它需要一个唯一名称(必填)作为第一个参数。这个名称是为了在 Devtools 中调试使用。
Store 有两种定义风格,类似于 Vue 的 Option API
和 Composition API
。
方式一:Option Store(选项式风格)
这种方式与 Vuex 的写法非常相似,更容易从 Vuex 迁移过来。
javascript
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 状态数据
state: () => ({
count: 0,
name: 'Eduardo',
}),
// 计算属性/派生状态
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne() { // 可以使用 this 访问整个 store 实例
return this.doubleCount + 1
},
},
// 操作方法(同步和异步)
actions: {
increment() {
this.count++ // 通过 this 访问 state
},
async incrementAsync() {
setTimeout(() => {
this.increment() // 可以调用其他 action
}, 1000)
},
},
})
方式二:Setup Store(组合式风格)
这种方式使用一个函数来定义 Store,类似于 Vue 的 setup()
函数和 Composition API。
javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state (使用 ref)
const count = ref(0)
const name = ref('Eduardo')
// getters (使用 computed)
const doubleCount = computed(() => count.value * 2)
const doubleCountPlusOne = computed(() => doubleCount.value + 1)
// actions (使用普通函数)
function increment() {
count.value++
}
async function incrementAsync() {
setTimeout(() => {
increment()
}, 1000)
}
// 返回所有需要暴露的状态和方法
return {
count,
name,
doubleCount,
doubleCountPlusOne,
increment,
incrementAsync,
}
})
如何选择?
- 如果你习惯 Vuex 或 Option API,选 Option Store。
- 如果你喜欢 Composition API 的灵活性,选 Setup Store。
5. 在组件中使用 Store
在组件中,你需要导入并调用 你定义的 Store 函数(例如 useCounterStore
)来使用它。Pinia 会自动管理单例。
访问 State 和 Getters
js
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double Count: {{ counter.doubleCount }}</p>
<p>Double Count Plus One: {{ counter.doubleCountPlusOne }}</p>
<p>Name: {{ name }}</p> <!-- 直接解构的 name -->
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia' // 重要!用于保持响应式
// 在 setup 中调用 Store 函数
const counter = useCounterStore()
// ❌ 错误:直接解构会失去响应式!
// const { count, name } = counter
// ✅ 正确:使用 storeToRefs 解构可以保持响应式
const { count, name } = storeToRefs(counter)
// 注意:actions 不需要也不应该用 storeToRefs 解构,直接通过 counter 调用即可
</script>
调用 Actions
js
<template>
<div>
<button @click="counter.increment()">Increment</button>
<button @click="counter.incrementAsync()">Increment Async</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// 也可以在方法或事件处理逻辑中调用
function handleClick() {
counter.increment()
}
</script>
6. 状态修改与响应式
- 直接修改: 你可以直接修改状态(
counter.count++
),这在 Pinia 中是允许的。 - 批量修改: 使用
$patch
方法进行批量更新,这有利于性能优化,并且 Devtools 会将其记录为一次修改。
javascript
const counter = useCounterStore()
// 方式一:传入一个部分 state 对象
counter.$patch({
count: counter.count + 1,
name: 'New Name',
})
// 方式二:传入一个修改函数,适用于修改数组或嵌套对象
counter.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
- 替换整个状态: 可以通过
$state
属性替换整个 store 的状态。
javascript
counter.$state = { count: 1000, name: 'Paimon' }
7. 高级特性与技巧
订阅状态变化
可以使用 $subscribe
来监听 state 及其变化,类似于 Vuex 的 subscribe。常用于持久化等操作。
javascript
counter.$subscribe((mutation, state) => {
// mutation 包含了修改的信息(events, type, storeId)
// state 是修改后的新状态
localStorage.setItem('counter', JSON.stringify(state))
})
订阅 Action
可以使用 $onAction
来监听 action 及其结果。
javascript
const unsubscribe = counter.$onAction(({ name, store, args, after, onError }) => {
// action 调用前执行
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// after 在 action 成功并返回后执行
after((result) => {
console.log(`Finished "${name}". Result was: ${result}.`)
})
// onError 在 action 抛出错误或 reject 时执行
onError((error) => {
console.warn(`Failed "${name}": ${error}.`)
})
})
// 手动移除订阅
// unsubscribe()
模块化(多 Store)
Pinia 天生是模块化的。你只需要定义多个不同的 Store 并在不同组件中引入它们即可。
javascript
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({ user: null }),
// ...
})
// stores/cart.js
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
// ...
})
然后在组件中可以同时使用多个 Store:
js
<script setup>
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
const userStore = useUserStore()
const cartStore = useCartStore()
// 甚至可以在一个 Store 中使用另一个 Store
// (注意避免循环引用)
</script>
总结:为什么选择 Pinia?
- 更简单的 API: 去除
mutations
,只有state
,getters
,actions
,概念更清晰。 - 完美的 TS 支持: 无需复杂配置,类型推断非常强大。
- 模块化设计: 不需要嵌套模块,多个 Store 自然拆分。
- 轻量级: 体积极小,对应用打包体积影响几乎可以忽略不计。
- Composition API: 与 Vue 3 的编程思想完美契合,使用起来更加灵活。
- 官方推荐: 作为 Vuex 的继任者,是未来 Vue 生态的状态管理首选。
对于新项目,强烈推荐直接使用 Pinia。对于老项目,如果 Vuex 能满足需求且没有维护性问题,不一定需要立即迁移,但 Pinia 绝对是未来趋势。