Skip to content

Pinia 详解:Vue 3 的状态管理方案

1. Pinia 简介

1.1 什么是 Pinia

Pinia 是 Vue 3 官方推荐的状态管理库,是 Vuex 的替代品。它提供了一种更简洁、更灵活的方式来管理应用的状态。

1.2 Pinia 的特点

  • 直观的 API:Pinia 提供了更简洁、更直观的 API,减少了模板代码
  • TypeScript 支持:内置 TypeScript 支持,提供更好的类型推导
  • 模块化设计:每个 store 都是一个独立的模块,可以单独导入和使用
  • 组合式 API 支持:完全支持 Vue 3 的组合式 API
  • 无 mutations:Pinia 移除了 mutations,直接在 actions 中修改状态
  • 支持插件:提供了丰富的插件系统

2. Pinia 安装与基本使用

2.1 安装 Pinia

bash
# 使用 npm
npm install pinia

# 使用 yarn
yarn add pinia

2.2 创建 Pinia 实例

javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

2.3 创建 Store

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: state => state.doubleCount + 1 // 可以使用其他 getter
  },
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    reset() {
      this.count = 0
    }
  }
})

2.4 在组件中使用 Store

vue
<template>
  <div>
    <h1>{{ counter.name }}</h1>
    <p>Count: {{ counter.count }}</p>
    <p>Double Count: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">Increment</button>
    <button @click="counter.decrement">Decrement</button>
    <button @click="counter.reset">Reset</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
</script>

3. Pinia 核心概念

3.1 State

State 是 store 的核心,存储应用的状态。在 Pinia 中,state 是一个返回初始状态的函数。

javascript
state: () => ({
  count: 0,
  user: {
    name: 'John',
    age: 30
  },
  items: []
})

3.2 Getters

Getters 是计算属性,用于从 state 中派生状态。

javascript
getters: {
  doubleCount: (state) => state.count * 2,
  // 可以访问其他 getters
  doubleCountPlusOne: (state, getters) => getters.doubleCount + 1,
  // 可以传递参数
  getItemById: (state) => (id) => {
    return state.items.find(item => item.id === id)
  }
}

3.3 Actions

Actions 是用于修改状态的方法,可以是同步的,也可以是异步的。

javascript
actions: {
  increment() {
    this.count++
  },
  // 异步 action
  async fetchItems() {
    const items = await api.fetchItems()
    this.items = items
  },
  // 可以调用其他 actions
  async refreshItems() {
    await this.fetchItems()
    console.log('Items refreshed')
  }
}

3.4 Store 组合

Pinia 允许在一个 store 中使用另一个 store。

javascript
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null
  }),
  actions: {
    setUser(user) {
      this.user = user
    }
  }
})

// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  actions: {
    addItem(item) {
      const userStore = useUserStore()
      if (userStore.user) {
        this.items.push(item)
      }
    }
  }
})

4. Pinia 实现原理

4.1 Store 的创建

Pinia 的 store 创建基于 defineStore 函数,它接收两个参数:

  1. Store 的唯一标识符
  2. Store 的配置对象(包含 state、getters、actions)
javascript
export function defineStore(id, options) {
  // 处理 options
  // 创建 store
  // 返回 useStore 函数
}

4.2 响应式原理

Pinia 使用 Vue 3 的 reactive API 来实现状态的响应式。

javascript
// 简化的实现
function createStore(options) {
  const state = reactive(options.state())

  const getters = Object.keys(options.getters || {}).reduce((acc, key) => {
    acc[key] = computed(() => options.getters[key](state))
    return acc
  }, {})

  const actions = Object.keys(options.actions || {}).reduce((acc, key) => {
    acc[key] = (...args) => options.actions[key].apply(state, args)
    return acc
  }, {})

  return {
    ...state,
    ...getters,
    ...actions
  }
}

4.3 Store 的注册与管理

Pinia 使用一个全局的 Pinia 实例来管理所有的 store。

javascript
// 简化的 Pinia 实现
class Pinia {
  constructor() {
    this.stores = new Map()
  }

  use(plugin) {
    // 注册插件
  }

  state

  _s
}

4.4 插件系统

Pinia 提供了插件系统,允许开发者扩展 Pinia 的功能。

javascript
// 示例插件
const myPlugin = context => {
  const { store } = context

  // 在 store 初始化时执行
  console.log('Store initialized:', store.$id)

  // 监听 store 的变化
  store.$subscribe((mutation, state) => {
    console.log('Mutation:', mutation)
    console.log('New state:', state)
  })
}

// 使用插件
pinia.use(myPlugin)

5. Pinia 与 Vuex 的对比

5.1 核心差异

特性PiniaVuex
Mutations移除了 mutations,直接在 actions 中修改状态需要通过 mutations 修改状态
模块化每个 store 都是独立的模块,自动命名空间需要手动配置命名空间
TypeScript 支持内置 TypeScript 支持,类型推导更友好需要手动添加类型定义
API 风格更简洁,更接近组合式 API更繁琐,基于选项式 API
代码分割自动支持代码分割需要手动配置
调试工具支持 Vue DevTools支持 Vue DevTools

5.2 语法对比

Vuex

javascript
// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },
  getters: {
    doubleCount: state => state.count * 2
  }
})

// 在组件中使用
import { mapState, mapActions, mapGetters } from 'vuex'

export default {
  computed: {
    ...mapState(['count']),
    ...mapGetters(['doubleCount'])
  },
  methods: {
    ...mapActions(['incrementAsync'])
  }
}

Pinia

javascript
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.increment()
    }
  },
  getters: {
    doubleCount: state => state.count * 2
  }
})

// 在组件中使用
import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const counter = useCounterStore()

    return {
      counter
    }
  }
}

5.3 性能对比

  • Pinia

    • 更小的包体积(约 1KB)
    • 更快的初始化速度
    • 更好的 tree-shaking 支持
    • 更高效的内存使用
  • Vuex

    • 更大的包体积
    • 较慢的初始化速度
    • 较差的 tree-shaking 支持
    • 较高的内存使用

5.4 迁移策略

从 Vuex 迁移到 Pinia 的步骤:

  1. 安装 Pinia
  2. 创建 Pinia 实例并注册到应用
  3. 将 Vuex 的 store 转换为 Pinia 的 store
  4. 更新组件中的使用方式
  5. 移除 Vuex 相关代码

6. Pinia 高级特性

6.1 持久化存储

使用 pinia-plugin-persistedstate 插件实现状态持久化。

bash
npm install pinia-plugin-persistedstate
javascript
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

// stores/counter.js
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  persist: true // 启用持久化
})

6.2 批量更新状态

使用 $patch 方法批量更新状态。

javascript
// 方法 1:对象形式
counter.$patch({
  count: counter.count + 1,
  name: 'Updated'
})

// 方法 2:函数形式
counter.$patch(state => {
  state.count++
  state.items.push({ id: 1, name: 'Item' })
})

6.3 重置状态

使用 $reset 方法重置状态到初始值。

javascript
counter.$reset()

6.4 监听状态变化

使用 $subscribe 方法监听状态变化。

javascript
counter.$subscribe((mutation, state) => {
  console.log('Mutation type:', mutation.type)
  console.log('Mutation storeId:', mutation.storeId)
  console.log('New state:', state)
})

6.5 订阅 Actions

使用 $onAction 方法订阅 Actions。

javascript
counter.$onAction(({ name, store, args, after, onError }) => {
  console.log(`Starting action ${name} with args:`, args)

  // 执行完毕后
  after(result => {
    console.log(`Action ${name} completed with result:`, result)
  })

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

7. Pinia 最佳实践

7.1 Store 设计

  1. 按功能划分 Store:每个 Store 负责一个特定的功能域
  2. 保持 Store 简洁:每个 Store 不要过于庞大
  3. 使用模块化:对于复杂的应用,使用多个 Store
  4. 合理使用 Getters:将复杂的计算逻辑放在 Getters 中
  5. 使用 Actions 处理业务逻辑:将业务逻辑封装在 Actions 中

7.2 性能优化

  1. 使用 storeToRefs:当只需要部分状态时,使用 storeToRefs 避免不必要的响应式
javascript
import { storeToRefs } from 'pinia'

const counter = useCounterStore()
const { count } = storeToRefs(counter) // 只对 count 进行响应式处理
  1. 合理使用 Getters 缓存:Getters 会自动缓存计算结果

  2. 避免在模板中直接调用 Actions:将 Actions 调用放在方法中

7.3 代码组织

  1. 创建 stores 目录:将所有的 Store 文件放在一个专门的目录中
  2. 使用命名规范:Store 文件名使用 kebab-case,Store 函数使用 useXXXStore 命名
  3. 导出 Store:每个 Store 文件导出一个 useXXXStore 函数

8. 常见问题与解决方案

8.1 问题:状态不更新

原因:可能是直接修改了状态对象的属性,而不是使用 Pinia 的方法。

解决方案

  • 使用 Actions 修改状态
  • 使用 $patch 方法批量更新状态
  • 确保状态对象是响应式的

8.2 问题:TypeScript 类型错误

原因:可能是类型定义不正确或缺少类型注解。

解决方案

  • 确保为 State 定义正确的类型
  • 使用 TypeScript 的类型推断
  • 为 Actions 和 Getters 添加类型注解

8.3 问题:持久化存储不生效

原因:可能是插件配置不正确或浏览器存储限制。

解决方案

  • 确保正确安装和配置 pinia-plugin-persistedstate
  • 检查浏览器存储是否有足够的空间
  • 检查状态是否可以被序列化

8.4 问题:Store 之间的循环依赖

原因:两个或多个 Store 相互引用。

解决方案

  • 重构 Store 结构,减少依赖
  • 在 Actions 中动态导入其他 Store
  • 使用一个公共的 Store 来管理共享状态

9. 完整示例

9.1 基本示例

javascript
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    token: null
  }),
  getters: {
    isLoggedIn: (state) => !!state.token
  },
  actions: {
    async login(username, password) {
      const response = await api.login(username, password)
      this.user = response.user
      this.token = response.token
    },
    logout() {
      this.user = null
      this.token = null
    }
  },
  persist: true
})

// stores/posts.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const usePostsStore = defineStore('posts', {
  state: () => ({
    posts: []
  }),
  getters: {
    userPosts: (state) => {
      const userStore = useUserStore()
      return state.posts.filter(post => post.userId === userStore.user?.id)
    }
  },
  actions: {
    async fetchPosts() {
      const response = await api.getPosts()
      this.posts = response.posts
    },
    async createPost(title, content) {
      const userStore = useUserStore()
      const response = await api.createPost({
        title,
        content,
        userId: userStore.user.id
      })
      this.posts.push(response.post)
    }
  }
})

// 在组件中使用
<template>
  <div>
    <div v-if="userStore.isLoggedIn">
      <h1>Welcome, {{ userStore.user.name }}</h1>
      <button @click="userStore.logout">Logout</button>

      <h2>Your Posts</h2>
      <div v-for="post in postsStore.userPosts" :key="post.id">
        <h3>{{ post.title }}</h3>
        <p>{{ post.content }}</p>
      </div>

      <button @click="fetchPosts">Fetch Posts</button>
    </div>
    <div v-else>
      <h1>Login</h1>
      <input v-model="username" placeholder="Username">
      <input v-model="password" type="password" placeholder="Password">
      <button @click="login">Login</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/stores/user'
import { usePostsStore } from '@/stores/posts'

const userStore = useUserStore()
const postsStore = usePostsStore()

const username = ref('')
const password = ref('')

const login = async () => {
  await userStore.login(username.value, password.value)
}

const fetchPosts = async () => {
  await postsStore.fetchPosts()
}
</script>

10. 总结

Pinia 是 Vue 3 官方推荐的状态管理库,它提供了一种更简洁、更灵活的方式来管理应用的状态。与 Vuex 相比,Pinia 具有以下优势:

  • 更简洁的 API:移除了 mutations,直接在 actions 中修改状态
  • 更好的 TypeScript 支持:内置 TypeScript 支持,提供更好的类型推导
  • 更灵活的模块化:每个 store 都是独立的模块,自动命名空间
  • 更好的性能:更小的包体积,更快的初始化速度,更好的 tree-shaking 支持
  • 更好的组合式 API 支持:完全支持 Vue 3 的组合式 API

Pinia 的设计理念更加符合 Vue 3 的编程风格,它的 API 更加直观和简洁,使得状态管理变得更加容易和愉快。对于新的 Vue 3 项目,Pinia 是一个更好的选择。

同时,Pinia 也提供了良好的向后兼容性,使得从 Vuex 迁移到 Pinia 变得更加容易。如果你正在使用 Vuex,并且考虑迁移到 Pinia,那么你会发现这个过程非常平滑和简单。

总之,Pinia 是一个现代化、高效、灵活的状态管理库,它为 Vue 3 应用提供了更好的状态管理解决方案。

基于 VitePress 的本地知识库