Pinia 13 个高级技巧:从入门到精通的核心实践

更新日期: 2026-03-30 阅读: 27 标签: Pinia

大多数人使用 Pinia,只用到了它三成的能力。

定义 store、读取 state、调用 action,然后就没有然后了。但 Pinia 真正的能量,藏在那些容易被忽视的地方。

一、$patch 函数模式:批量原子更新

大多数人这样使用 $patch:

store.$patch({ count: 10, name: 'Alice' })

这没有问题。但遇到数组操作时,问题就出现了:

// ❌ 这会替换整个数组,触发多次响应式更新
store.$patch({ items: [...store.items, newItem] })

正确的做法是使用函数模式:

// ✅ 直接操作,单次响应,性能更好
store.$patch((state) => {
  state.items.push(newItem)
  state.count += 1
  state.loading = false
})

函数模式的核心在于:在一次“事务”中完成所有修改,只触发一次响应式更新。集合操作、条件判断、多字段联动,都应优先采用函数模式。

二、$subscribe 的 detached 模式:脱离组件的监听

$subscribe 的默认行为是:组件卸载时,订阅自动销毁。这在大多数场景下足够使用。但对于持久化存储这类需求,你需要订阅在组件消失后继续存在。加上 { detached: true } 即可实现:

const unsub = store.$subscribe(
  (mutation, state) => {
    // mutation.type: 'direct' | 'patch object' | 'patch function'
    // mutation.storeId: 哪个 store 触发的
    // mutation.events: 具体变更的 key(仅 direct 模式有效)

    localStorage.setItem('store-cache', JSON.stringify(state))
  },
  { detached: true }  // 组件卸载后不停止
)

// 手动停止
unsub()

注意:detached 订阅不会自动清理,忘记调用 unsub() 可能导致内存泄漏。

三、$onAction 三段钩子:Action 全生命周期

$onAction 不只是“监听 action 被调用”。它提供了三个关键时间点:调用前、成功后、出错时。

store.$onAction(({
  name,     // action 名称
  args,     // 调用参数
  after,    // 成功后的钩子
  onError,  // 出错后的钩子
}) => {
  const startTime = Date.now()

  after((result) => {
    const duration = Date.now() - startTime
    // 性能埋点、日志上报
    analytics.track('action_success', { name, duration })
  })

  onError((error) => {
    // 错误监控
    Sentry.captureException(error)
  })
})

这是实现埋点、性能监控、错误上报的最干净方式——业务 action 本身保持纯净,横切逻辑全部在此处理。

四、storeToRefs:解构 store 的正确姿势

这是新手最容易踩的坑。

// ❌ 直接解构,响应性丢失
const { count, name } = useUserStore()
// count 和 name 变成了普通值,不会随 store 更新

正确做法:

// ✅ storeToRefs 只提取 state/getter 为 ref
const store = useUserStore()
const { count, name } = storeToRefs(store)

// action 直接从 store 取,不需要包裹
const { fetchUser } = store

原理:storeToRefs 会将每个 state 属性转换为 toRef(store, key),保持与原 store 的双向绑定。

五、$dispose:手动销毁 Store

大多数开发者不知道 store 可以被销毁。调用 $dispose() 会做三件事:停止所有订阅、从 pinia 实例中移除、释放内存。

async function logout() {
  await authAPI.logout()

  // 销毁所有用户相关 store
  useUserStore().$dispose()
  useCartStore().$dispose()
  useOrderStore().$dispose()

  router.push('/login')
}

在登出场景、路由切换时清理临时 store,可以使用此方法。

六、Setup Store 手写 $reset

Options Store 自带 $reset() 方法,可一键恢复初始状态。Setup Store 没有内置,需要自己实现:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name  = ref('Eve')

  function $reset() {
    count.value = 0
    name.value  = 'Eve'
  }

  return { count, name, $reset }
})

如果初始值来自外部配置,可以用闭包将初始快照存储起来:

export const useFormStore = defineStore('form', () => {
  const initialState = { title: '', content: '', tags: [] }
  const form = reactive({ ...initialState })

  function $reset() {
    Object.assign(form, initialState)
  }

  return { form, $reset }
})

七、getActivePinia:组合式函数外访问 Store

在组件的 setup() 里使用 useXxxStore() 没有问题。但在 class、工具函数、setTimeout 回调里调用可能会报错:getActivePinia was called with no active Pinia。

解决方案:

import { getActivePinia } from 'pinia'

class ApiService {
  getAuthToken() {
    const pinia = getActivePinia()
    if (!pinia) throw new Error('Pinia 未激活')

    const authStore = useAuthStore(pinia)
    return authStore.token
  }
}

将 pinia 实例作为参数传给 useXxxStore(pinia),即可在任意上下文中安全访问。

八、Store 间引用:在 Action 内部懒加载

两个 store 互相引用是常见需求,也是循环依赖的高发区。

// ❌ 危险:文件顶层互相 import 可能导致循环依赖
const userStore = useUserStore()  // 顶层调用

export const useOrderStore = defineStore('order', {
  actions: {
    async fetchOrders() { /* ... */ }
  }
})

正确做法是将引用放到 action 内部,按需加载:

// ✅ action 内部懒加载,天然避免循环依赖
export const useOrderStore = defineStore('order', {
  actions: {
    async fetchOrders() {
      const userStore = useUserStore()  // 在这里调用,而不是顶层
      const userId = userStore.id
      return await api.getOrders(userId)
    }
  }
})

这不是权宜之计,而是 Pinia 官方推荐的模式。

九、defineStore 自定义元数据:让插件感知 Store 意图

defineStore 的第三个参数 options 支持任意自定义字段。插件通过 context.options 读取,实现声明式配置。

// 在 store 上声明配置
export const useSearchStore = defineStore(
  'search',
  {
    state: () => ({ query: '', results: [] }),
    actions: {
      async search(q: string) { /* ... */ }
    }
  },
  {
    // 自定义元数据,供插件消费
    debounce: { search: 300 },
    persist: { key: 'search-cache', storage: sessionStorage },
  }
)

// 插件中读取并处理
pinia.use(({ options, store }) => {
  if (options.debounce) {
    return Object.keys(options.debounce).reduce((acc, action) => {
      acc[action] = debounce(store[action], options.debounce[action])
      return acc
    }, {})
  }
})

这是实现“约定大于配置”的核心机制,持久化插件、防抖插件都依赖于此。

十、acceptHMRUpdate:热更新不丢状态

使用 Vite 开发时,修改 store 文件会导致页面状态重置。添加三行代码即可解决:

import { acceptHMRUpdate, defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  // ...
})

// 文件底部加上这三行
if (import.meta.hot) {
  import.meta.hot.accept(
    acceptHMRUpdate(useCounterStore, import.meta.hot)
  )
}

修改 store 代码后,状态得以保留,页面不刷新。调试体验会有明显提升。

十一、setActivePinia:单元测试中的 Store 隔离

测试 store 时最常见的问题是测试用例之间的状态污染。每个用例前创建全新的 pinia 实例,可彻底隔离:

import { setActivePinia, createPinia } from 'pinia'
import { beforeEach, test, expect } from 'vitest'
import { useCounterStore } from './counter'

beforeEach(() => {
  // 每个测试用例前重置 pinia
  setActivePinia(createPinia())
})

test('increments counter', () => {
  const store = useCounterStore()
  expect(store.count).toBe(0)

  store.increment()
  expect(store.count).toBe(1)
})

test('starts at zero', () => {
  const store = useCounterStore()
  // 不受上一个测试影响
  expect(store.count).toBe(0)
})

配合 createTestingPinia 还可以 mock action,让测试更纯粹。

十二、$state 直接替换:SSR 注水与快照恢复

服务端渲染时,需要将服务端的状态“注入”到客户端 store。

const store = useUserStore()

// 从服务端传来的初始数据,批量恢复
store.$patch(window.__INITIAL_STATE__.user)

也可以直接给 $state 赋值(Pinia 内部会调用 $patch):

store.$state = JSON.parse(localStorage.getItem('user-snapshot'))

这在实现“页面快照保存与恢复”的功能时非常有用。

十三、markRaw:注入外部服务时阻止响应式

将 router、axios 等外部对象注入 store 是常见做法。但 Vue 会尝试将它们转换为响应式对象——这既浪费性能,有时还会引发错误。使用 markRaw 包裹,明确告诉 Vue 这个对象不需要响应式处理。

import { markRaw } from 'vue'
import { router } from './router'

pinia.use(({ store }) => {
  store.router = markRaw(router)
  store.axios  = markRaw(axiosInstance)
})

这通常写在 Pinia 插件里,一次配置,所有 store 都能通过 this.router 和 this.axios 访问。

总结

这 13 个技巧,覆盖了 Pinia 从日常使用到插件开发的完整深度:

场景技巧
状态更新$patch 函数模式
状态监听$subscribe + detached
Action 追踪$onAction 三段钩子
响应式解构storeToRefs
Store 销毁$dispose
状态重置Setup Store 手写 $reset
非 setup 访问getActivePinia
跨 store 引用Action 内部懒加载
插件配置化defineStore 自定义元数据
开发体验acceptHMRUpdate
测试隔离setActivePinia
SSR/快照$state 直接替换
外部服务注入markRaw

大多数人停留在了第一层。真正用好 Pinia,需要了解这些。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13525

相关推荐

测试一下Pinia,Vuex 要出局了?

自从我开始使用Vue 3和组合API以来,我也尝试使用 Pinea 作为状态管理库。如果是从是 vue2 和 vuex 过来的,就会觉得用起来差别还是很大的。

全新的 Vue3 状态管理工具:Pinia

Vue3 发布已经有一段时间了,它采用了新的响应式系统,而且构建了一套全新的 Composition API。Vue 的周边生态都在加紧适配这套新的系统,官方的状态管理库 Vuex 也在适配中

快速入门Pinia状态管理库

Pinia 是一个用于 Vue 的状态治理库,相似 Vuex, 是 Vue 的另一种状态治理计划。如果你现在使用 vue3 开发项目,那么推荐你使用 Pinia 开发。

Vue新一代状态管理插件Pinia

如果你之前使用过 vuex 进行状态管理的话,那么 pinia 就是一个类似的插件。它是最新一代的轻量级状态管理插件。按照尤雨溪的说法,vuex 将不再接收新的功能,建议将 Pinia 用于新的项目。

Pinia是Vuex的良好替代品吗?

Pinia 是 Vue.js 的轻量级状态管理库,最近很受欢迎。它使用 Vue 3 中的新反应系统来构建一个直观且完全类型化的状态管理库。

Pinia被鼓吹的setup写法真的好用吗

引用官方的一句话:Pinia是一个符合直觉的 Vue.js 状态管理库。简单说几点它的特性:它支持Vue3,同时也支持Vue2,是Vuex的完美过渡替代者

一文解析 Pinia 和 Vuex ,带你全面理解这两个 Vue 状态管理模式

Pinia和Vuex一样都是是vue的全局状态管理器。其实Pinia就是Vuex5,只不过为了尊重原作者的贡献就沿用了这个看起来很甜的名字Pinia。本文将通过Vue3的形式对两者的不同实现方式进行对比,让你在以后工作中无论使用到Pinia还是Vuex的时候都能够游刃有余。

我使用 Pinia 的 5 大技巧

在这篇文章中,想与大家分享使用 Pinia 的五大技巧。以下是简要总结:不要创建无用的 getter,在 Option Stores 中使用组合式函数(composables),对于复杂的组合式函数,使用 Setup Stores

Pinia 3.0 正式发布,不再支持 Vue 2

Vue 官方推荐的状态管理工具 Pinia 最近更新到了 3.0 版本。这个版本有一个最重要的变化:它彻底放弃了对 Vue 2 的支持,现在只专注于 Vue 3 的生态。

Vue状态管理:Pinia与Vuex全面对比

在Vue应用开发中,随着项目规模的增长,组件之间的数据共享变得越来越复杂。状态管理工具就是为了解决这个问题而出现的。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!