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

概念VuexPinia说明
状态statestate存储应用状态数据的地方
获取状态gettersgetters用于计算和派生状态,相当于 Store 的计算属性
修改状态mutationsactions这是最大的不同。Pinia 中 actions 既可用于同步,也可用于异步操作
异步操作actionsactions在 Pinia 中,异步和同步操作都在 actions 中完成

简单来说,Pinia 就是:state + getters + actions


3. 安装与设置

1. 安装:

npm install pinia
# 或
yarn add pinia

2. 在 Vue 应用中创建并引入 Pinia:

main.jsmain.ts 中:

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 APIComposition API

方式一:Option Store(选项式风格)

这种方式与 Vuex 的写法非常相似,更容易从 Vuex 迁移过来。

// 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。

// 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
<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
<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 会将其记录为一次修改。
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 的状态。
counter.$state = { count: 1000, name: 'Paimon' }

7. 高级特性与技巧

订阅状态变化

可以使用 $subscribe 来监听 state 及其变化,类似于 Vuex 的 subscribe。常用于持久化等操作。

counter.$subscribe((mutation, state) => {
  // mutation 包含了修改的信息(events, type, storeId)
  // state 是修改后的新状态
  localStorage.setItem('counter', JSON.stringify(state))
})
订阅 Action

可以使用 $onAction 来监听 action 及其结果。

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 并在不同组件中引入它们即可。

// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({ user: null }),
  // ...
})

// stores/cart.js
export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  // ...
})

然后在组件中可以同时使用多个 Store:

<script setup>
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

const userStore = useUserStore()
const cartStore = useCartStore()
// 甚至可以在一个 Store 中使用另一个 Store
// (注意避免循环引用)
</script>

总结:为什么选择 Pinia?

  1. 更简单的 API: 去除 mutations,只有 state, getters, actions,概念更清晰。
  2. 完美的 TS 支持: 无需复杂配置,类型推断非常强大。
  3. 模块化设计: 不需要嵌套模块,多个 Store 自然拆分。
  4. 轻量级: 体积极小,对应用打包体积影响几乎可以忽略不计。
  5. Composition API: 与 Vue 3 的编程思想完美契合,使用起来更加灵活。
  6. 官方推荐: 作为 Vuex 的继任者,是未来 Vue 生态的状态管理首选。

对于新项目,强烈推荐直接使用 Pinia。对于老项目,如果 Vuex 能满足需求且没有维护性问题,不一定需要立即迁移,但 Pinia 绝对是未来趋势。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐