第13章 Pinia 状态管理

第十三章 Pinia 状态管理

当应用规模变大,组件之间需要共享的数据会越来越多——用户登录信息、全局主题配置、购物车内容、通知列表……这些数据放在组件里会导致层层 props 穿透,放全局变量会失去响应式。Pinia 就是来解决这个问题的。Pinia 是 Vue 官方推荐的新一代状态管理库,它比 Vuex 更简洁、更 TypeScript 友好,是 Vue 3 项目的不二之选。

13.1 什么是状态管理

13.1.1 为什么需要 Pinia

想象一个场景:你的应用有一个购物车功能。购物车的商品列表在多个页面都需要展示——商品列表页、商品详情页、结算页、头部导航栏。不用状态管理的话,每个页面都要 props 层层传递,代码会变成这样噩梦般的结构:

flowchart TB
    A["App.vue"] --> B["Header.vue<br/>购物车数量显示"]
    B --> |"props"| C["ProductList.vue"]
    C --> |"props"| D["ProductCard.vue"]
    D --> |"props + emit"| C
    C --> |"props"| E["CartDrawer.vue"]
    E --> |"props"| F["CartItem.vue"]

这种层层传递叫 prop drilling,组件被 props 穿透得千疮百孔,而且 props 路径上经过的中间组件可能根本不需要这些数据。

Pinia(以及它的前身 Vuex)就是来解决这个问题的——把全局需要共享的数据放到一个"仓库"(store)里,所有组件想用就用,不需要层层传递。

13.1.2 Pinia vs Vuex vs 组件直接通信

方案适用场景复杂度推荐度
组件直接通信(props + emit)父子通信,数据不需要共享✅ 父子通信首选
Provide / Inject跨多层共享,不需要全局✅ 局部的全局状态
Pinia全局共享,复杂应用✅ Vue 3 推荐
Vuex全局共享(Vue 2 / 旧项目)⚠️ Vue 2 项目

Pinia 相比 Vuex 的核心优势:API 更简单(去掉了 mutation、module 的嵌套结构),支持 TypeScript 的完美类型推导,体积更小(< 1KB)。

13.2 快速上手

13.2.1 安装与配置

1
pnpm add pinia
1
2
3
4
5
6
7
8
9
// src/main.ts
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)

app.use(createPinia())  // 注册 Pinia

app.mount('#app')

13.2.2 创建第一个 Store

Pinia 的 Store 用选项式setup 语法两种写法都可以。

选项式写法(类似 Vuex):

 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
28
29
30
31
32
33
34
35
36
37
// src/stores/counter.ts
import { defineStore } from 'pinia'

// defineStore() 第一个参数是 store 的唯一 ID(必须唯一!)
// 第二个参数是 store 的定义(可以是对象或函数)
export const useCounterStore = defineStore('counter', {
  // state:定义响应式状态数据(相当于 Vue 2 的 data)
  // 必须是一个返回函数的箭头函数 () => ({}),不能是普通对象
  // 这样 Pinia 能追踪其中的响应式数据
  state: () => ({
    count: 0,  // 计数器当前值
    step: 1     // 每次增加/减少的步长
  }),

  // getters:计算属性(相当于 Vue 2 的 computed)
  // Pinia 的 getter 会自动缓存(当依赖的 state 没变时,不会重新计算)
  getters: {
    // state 是 Pinia 自动注入的第一个参数,不用 import
    doubled: (state) => state.count * 2,  // count * 2,访问方式:store.doubled
    triple: (state) => state.count * 3   // count * 3,访问方式:store.triple
  },

  // actions:修改 state 的方法(相当于 Vue 2 的 methods)
  // actions 里可以写同步或异步逻辑
  // 注意:getters 可以通过 this.访问其他 getters,state 通过 this. 访问 state
  actions: {
    increment() {
      this.count += this.step  // this 指向整个 store 实例
    },
    decrement() {
      this.count -= this.step
    },
    reset() {
      this.count = 0
    }
  }
})

setup 语法写法(更接近 Composition API):

 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
28
// src/stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)
  const step = ref(1)

  // getters
  const doubled = computed(() => count.value * 2)
  const triple = computed(() => count.value * 3)

  // actions
  function increment() {
    count.value += step.value
  }

  function decrement() {
    count.value -= step.value
  }

  function reset() {
    count.value = 0
  }

  return { count, step, doubled, triple, increment, decrement, reset }
})

13.2.3 在组件中使用 Store

 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
<script setup>
import { useCounterStore } from '@/stores/counter'

// 一行代码获取 store
const counterStore = useCounterStore()

// 访问 state
console.log(counterStore.count)  // 0

// 访问 getter
console.log(counterStore.doubled)  // 0

// 调用 action
counterStore.increment()

// 直接修改 state(Pinia 支持直接修改,比 Vuex 的 mutation 灵活)
counterStore.count = 10
</script>

<template>
  <div>
    <p>计数{{ counterStore.count }}</p>
    <p>双倍{{ counterStore.doubled }}</p>
    <button @click="counterStore.increment">+1</button>
  </div>
</template>

13.2.4 Store 目录结构规范

src/
├── stores/
│   ├── index.ts          # 统一导出
│   ├── counter.ts        # counter store
│   ├── user.ts           # user store
│   ├── cart.ts           # cart store
│   └── settings.ts        # settings store
1
2
3
4
5
// src/stores/index.ts
export { useCounterStore } from './counter'
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useSettingsStore } from './settings'

13.3 State

13.3.1 定义与读取(直接访问 / storeToRefs)

Pinia 的 state 是响应式的,组件里访问时直接用 store.xxx 即可。

1
2
3
4
5
6
7
8
9
// 选项式 store
state: () => ({
  count: 0,
  name: '小明',
  items: [] as string[]
})

// setup 语法
const count = ref(0)

storeToRefs:当你需要解构 store 的属性时,用 storeToRefs 保持响应式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()

// 直接解构会丢失响应式
// const { count } = counterStore  // ❌ count 不是响应式的

// 用 storeToRefs 解构,保持响应式
const { count, doubled } = storeToRefs(counterStore)

// actions 可以直接解构,不需要 storeToRefs
const { increment } = counterStore

13.3.2 重置 state

1
2
3
4
5
6
7
// 选项式:调用 $reset() 方法(只有选项式写法有这个方法)
counterStore.$reset()

// setup 语法:自己实现 reset
function reset() {
  count.value = 0
}

13.3.3 修改 state(直接修改 vs $patch)

Pinia 支持两种修改 state 的方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 方式一:直接修改(最简单)
counterStore.count = 10

// 方式二:$patch(适合批量修改)
counterStore.$patch({
  count: 20,
  name: '小红'
})

// 方式三:$patch 接收函数(适合复杂逻辑)
cartStore.$patch((state) => {
  state.items.push({ id: Date.now(), name: '新商品', price: 99 })
  state.total = state.items.reduce((sum, item) => sum + item.price, 0)
})

13.3.4 $patch(对象 / 函数两种写法)

1
2
3
4
5
6
7
8
// 对象写法:适合批量更新多个字段
store.$patch({ count: 0, name: '重置' })

// 函数写法:适合复杂的状态计算
store.$patch((state) => {
  // 可以做任何逻辑
  state.history.push(state.current)
})

13.4 Getters

13.4.1 基本用法

选项式 getters:

1
2
3
4
getters: {
  doubled: (state) => state.count * 2,
  greeting: (state) => `你好,${state.name}!`
}

setup 语法:

1
2
const doubled = computed(() => count.value * 2)
const greeting = computed(() => `你好,${name.value}!`)

13.4.2 访问其他 getter

选项式:

1
2
3
4
getters: {
  doubled: (state) => state.count * 2,
  quadruple: (state) => state.doubled * 2  // 访问其他 getter
}

**setup 语法:**直接在 computed 里引用即可,因为 computed 是在同一个函数作用域里。

13.4.3 返回函数(参数化 getter)

1
2
3
4
5
6
7
8
9
// 选项式:getter 可以返回一个函数
getters: {
  getItemById: (state) => {
    return (id: number) => state.items.find(item => item.id === id)
  }
}

// 使用
const item = userStore.getItemById(1)

13.4.4 缓存机制

Pinia 的 getter 和 Vue 的 computed 一样,有缓存机制——只有当依赖的 state 变化时才会重新计算,多次访问同一个 getter 不会重复计算。

13.5 Actions

13.5.1 同步 Actions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 选项式
actions: {
  increment() {
    this.count++
  },
  setName(name: string) {
    this.name = name
  }
}

// setup 语法
function setName(name: string) {
  name.value = name
}

13.5.2 异步 Actions

Pinia 的 actions 完美支持 async/await:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 选项式
actions: {
  async fetchUser(id: string) {
    this.isLoading = true
    try {
      const user = await api.getUser(id)
      this.currentUser = user
    } catch (err) {
      this.error = err as Error
    } finally {
      this.isLoading = false
    }
  }
}

13.5.3 async/await 使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// setup 语法
async function fetchUser(id: string) {
  isLoading.value = true
  try {
    const user = await api.getUser(id)
    currentUser.value = user
  } catch (err) {
    error.value = err as Error
  } finally {
    isLoading.value = false
  }
}

// 组件里调用
async function loadUser() {
  await userStore.fetchUser('123')
}

13.5.4 Actions 相互调用

同一个 store 里,action 可以直接调用其他 action:

1
2
3
4
5
6
7
8
9
// 选项式
actions: {
  async fetchUserData(id: string) {
    this.isLoading = true
    await this.fetchUser(id)          // 调用其他 action
    await this.fetchUserPosts(id)     // 调用另一个 action
    this.isLoading = false
  }
}

13.5.5 $onAction 监听 Actions

Pinia 提供 $onAction 来监听 store 里 action 的执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const store = useCounterStore()

store.$onAction(({ name, args, after, onError }) => {
  console.log(`Action "${name}" 即将执行,参数:`, args)

  after((returnValue) => {
    console.log(`Action "${name}" 执行完成,返回值:`, returnValue)
  })

  onError((error) => {
    console.error(`Action "${name}" 执行出错:`, error)
  })
})

13.6 Store 解构与响应式

13.6.1 storeToRefs 的使用

Pinia 的 store 是响应式的 Proxy,直接解构会丢失响应式。用 storeToRefs 可以保持解构后的值仍然是响应式的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 解构 state 和 getters 用 storeToRefs
const { name, age, isAdult } = storeToRefs(userStore)

// 解构 actions 不需要 storeToRefs
const { setName, fetchUser } = userStore

13.6.2 解构后响应式丢失问题

1
2
3
4
5
6
7
// ❌ 错误:直接解构,响应式丢失
const { count } = useCounterStore()
count.value++  // 不生效!

// ✅ 正确:解构后仍然是 ref
const { count } = storeToRefs(useCounterStore())
count.value++  // 生效

13.6.3 setup 语法糖中使用 Store

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// state 和 getters 用 storeToRefs
const { name, isLoggedIn } = storeToRefs(userStore)

// actions 直接用
const login = userStore.login
const logout = userStore.logout
</script>

13.7 模块化

13.7.1 多模块划分

Pinia 不需要像 Vuex 那样嵌套 module,直接创建多个 store 文件即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({ name: '', token: '' }),
  actions: { login() { ... } }
})

// stores/cart.ts
export const useCartStore = defineStore('cart', {
  state: () => ({ items: [], total: 0 }),
  actions: { addItem() { ... } }
})

13.7.2 setup Store 写法

setup 语法写法天然支持"模块化"——每个 store 就是一个独立函数,不需要额外配置。

13.7.3 模块间相互调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
  const { isLoggedIn } = storeToRefs(useUserStore())

  function checkout() {
    if (!isLoggedIn.value) {
      // 调用其他 store
      useUserStore().showLoginModal()
      return false
    }
    // 正常结账
    return true
  }

  return { checkout }
})

13.8 插件机制

13.8.1 Pinia 插件介绍

Pinia 支持插件机制,可以用来扩展 store 的功能,比如持久化、本地存储同步等。

Pinia 插件的本质是一个函数,接收 { store } 对象,可以给每个 store 动态添加属性或方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { createPinia } from 'pinia'

const pinia = createPinia()

// pinia.use():注册插件,插件会在每个 store 创建时执行
// 回调参数 { store } 包含当前 store 实例
pinia.use(({ store }) => {
  // 给每个 store 动态添加一个 createdAt 属性
  // 注意:动态添加的属性不是响应式的(除非用 ref/reactive 包装)
  store.createdAt = Date.now()
})

插件能做什么?

  • 给每个 store 动态添加属性(如 createdAt
  • 监听 store 的 $subscribe 事件(state 变化时做额外处理)
  • 包装 store.$actions 里的方法(注入日志、错误处理等)
  • 注入 $http 等全局服务

13.8.2 持久化插件(pinia-plugin-persistedstate)

页面刷新后,Pinia store 的数据会丢失——这是因为 store 数据存在内存里。pinia-plugin-persistedstate 插件把 store 数据自动保存到 localStorage,刷新后自动恢复。

1
pnpm add pinia-plugin-persistedstate
1
2
3
4
5
6
7
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
// 注册插件:这一步必须在 createPinia() 后立即执行
pinia.use(piniaPluginPersistedstate)
1
2
3
4
5
6
7
// store 里开启持久化
export const useSettingsStore = defineStore('settings', {
  state: () => ({ theme: 'light', language: 'zh-CN' }),
  persist: true  // 开启持久化:刷新页面后 theme 和 language 会自动从 localStorage 恢复
  // persist: { key: 'my-settings' }  // 可选:指定 localStorage 的 key 名
  // persist: { storage: sessionStorage }  // 可选:用 sessionStorage 替代 localStorage
})

13.8.3 自定义插件实战

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 插件:自动打印每个 action 的调用日志(方便调试)
pinia.use(({ store }) => {
  // store.$actions:包含 store 里所有 action 方法的对象
  const originalActions = { ...store.$actions }

  // 遍历每个 action,包装成带日志的版本
  Object.keys(originalActions).forEach((actionName) => {
    const originalAction = originalActions[actionName]

    // 覆盖原 action,注入日志逻辑
    ;(store as any)[actionName] = async (...args: any[]) => {
      // action 执行前打印日志
      console.log(`[${store.$id}] ${actionName} 开始了,参数:`, args)
      try {
        const result = await originalAction.apply(store, args)
        console.log(`[${store.$id}] ${actionName} 完成了`)
        return result
      } catch (error) {
        console.error(`[${store.$id}] ${actionName} 出错了`, error)
        throw error
      }
    }
  })
})

13.9 进阶用法

13.9.1 $subscribe 订阅 state 变化

1
2
3
4
5
6
7
const store = useCounterStore()

// 监听 state 变化(类似 watch)
store.$subscribe((mutation, state) => {
  console.log('state 变化了:', mutation)
  console.log('当前 state:', state)
}, { detached: true })  // detached: true 表示组件卸载后仍然监听

13.9.2 热更新支持

Pinia 支持 HMR(热模块替换):

1
2
3
4
5
6
7
8
// stores/counter.ts
if (import.meta.hot) {
  import.meta.hot.accept(import.meta.url, () => {
    const newStore = require('./counter').useCounterStore
    // 替换 store
    pinia._s.get('counter') !== newStore(pinia)
  })
}

13.10 调试(Vue DevTools)

Pinia 完美集成 Vue DevTools,可以在 DevTools 的 Pinia 面板里:

  • 查看所有 store 的 state
  • 手动修改 state(实时预览效果)
  • 查看 getters 的计算结果
  • 查看 action 执行历史
  • 时间旅行调试(回溯 state 历史)

本章小结

本章我们系统学习了 Pinia 状态管理:

  • 为什么需要状态管理:解决 prop drilling 问题,让全局数据集中管理。
  • Pinia vs Vuex:Pinia API 更简洁、TypeScript 支持更好、体积更小。
  • 快速上手:createPinia、defineStore 的两种写法(选项式 vs setup 语法)。
  • State:定义、读取、storeToRefs 解构、重置、$patch 批量修改。
  • Getters:缓存机制、访问其他 getter、参数化 getter。
  • Actions:同步/异步 action、actions 相互调用、$onAction 监听。
  • Store 解构:storeToRefs 保持响应式,actions 不需要 storeToRefs。
  • 模块化:多个 store 文件直接使用,不需要 Vuex 那样的 module 嵌套。
  • 插件机制:pinia-plugin-persistedstate 持久化、自定义插件。
  • 调试:Vue DevTools 的 Pinia 面板。

下一章我们会学习 性能优化——这是区分"会用 Vue"和"用好 Vue"的关键章节。路由懒加载、组件缓存、虚拟滚动、Tree Shaking、Core Web Vitals 优化……让你成为真正的 Vue 性能高手!