第8章 状态管理

第23节 State与Getters

8.23.1 状态定义与响应式

状态定义

在Pinia中,状态是通过state选项定义的,它是一个返回初始状态对象的函数。这种函数形式确保了每个组件实例都能获得独立的状态副本。

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

export const useUserStore = defineStore('user', {
  state: () => ({
    // 基本类型
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    age: 30,
    isActive: true,
    
    // 数组类型
    roles: ['user', 'editor'],
    permissions: ['read', 'write'],
    
    // 对象类型
    profile: {
      avatar: 'https://example.com/avatar.jpg',
      bio: '热爱技术的开发者',
      location: '北京'
    },
    
    // 嵌套对象
    settings: {
      notifications: {
        email: true,
        sms: false,
        push: true
      },
      theme: 'light',
      language: 'zh-CN'
    }
  })
})

响应式状态自动解包

直接访问状态

在组件中使用Store时,Pinia会自动解包状态属性,我们可以直接访问它们,不需要使用.value(与Vue 3的ref不同)。

<template>
  <div>
    <h1>{{ userStore.name }}</h1> <!-- 直接访问,不需要.userStore.name.value -->
    <p>{{ userStore.age }}</p> <!-- 直接访问 -->
  </div>
</template>

<script setup>
import { useUserStore } from '../stores/user'

const userStore = useUserStore()
</script>
解构状态

当我们需要解构状态时,需要使用storeToRefs函数来保持响应性,否则解构后的属性将失去响应性。

// 错误示例:解构后失去响应性
const { name, age } = userStore
name = '李四' // 不会更新Store中的状态

// 正确示例:使用storeToRefs保持响应性
import { storeToRefs } from 'pinia'
const { name, age } = storeToRefs(userStore)
name.value = '李四' // 会更新Store中的状态

状态重置

使用$reset方法可以将Store的状态重置为初始值,这在需要重新初始化状态时非常有用。

// 重置所有状态
userStore.$reset()

// 示例:在组件中使用
const resetUser = () => {
  userStore.$reset()
}

状态变更

Pinia提供了多种方式来修改状态,我们可以根据不同的场景选择合适的方式:

  1. 直接修改:适合简单的状态更新

    userStore.name = '李四'
    userStore.age++
    userStore.isActive = false
  2. 使用$patch对象形式:适合同时修改多个状态

    userStore.$patch({
      name: '李四',
      age: 31,
      isActive: false
    })
  3. 使用$patch函数形式:适合复杂的状态更新,如修改数组或嵌套对象

    userStore.$patch((state) => {
      // 修改数组
      state.roles.push('admin')
      state.permissions = [...state.permissions, 'delete']
      
      // 修改嵌套对象
      state.settings.theme = 'dark'
      state.settings.notifications.sms = true
      
      // 复杂计算
      state.age = new Date().getFullYear() - 1990
    })
  4. 使用Actions:适合包含业务逻辑或异步操作的状态更新

    // 在Store中定义Action
    actions: {
      updateUserProfile(profileData) {
        // 可以添加业务逻辑
        if (profileData.age < 18) {
          throw new Error('年龄必须大于18岁')
        }
        
        // 更新状态
        this.$patch({
          name: profileData.name,
          age: profileData.age,
          profile: {
            ...this.profile,
            ...profileData
          }
        })
      }
    }
    
    // 在组件中使用
    userStore.updateUserProfile({
      name: '李四',
      age: 31,
      bio: '更新后的个人简介'
    })

8.23.2 Getters计算属性

Getters是Store中的计算属性,它们可以基于State派生出新的值。Getters会被缓存,只有当依赖的State发生变化时才会重新计算。

基本Getters

// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    id: 1,
    name: '张三',
    age: 30,
    roles: ['user', 'editor'],
    permissions: ['read', 'write']
  }),
  
  getters: {
    // 简单Getters
    isAdult: (state) => state.age >= 18,
    isAdmin: (state) => state.roles.includes('admin'),
    roleCount: (state) => state.roles.length,
    
    // 使用模板字符串
    userInfo: (state) => `${state.name} (${state.age}岁)`,
    
    // 过滤数组
    activePermissions: (state) => state.permissions.filter(perm => perm !== 'delete')
  }
})

访问其他Getters

在Getters中,我们可以使用this访问其他Getters,这允许我们组合多个Getters来创建更复杂的计算逻辑。

getters: {
  isAdult: (state) => state.age >= 18,
  
  // 使用this访问其他Getters
  canEdit: function(state) {
    return this.isAdult && state.roles.includes('editor')
  },
  
  // 箭头函数中不能使用this,需要直接访问state
  canManageUsers: (state) => {
    // 错误:箭头函数中this不指向Store实例
    // return this.isAdmin && this.isAdult
    
    // 正确:直接基于state计算
    return state.roles.includes('admin') && state.age >= 18
  }
}

传递参数给Getters

Getters本身不能接受参数,但我们可以返回一个函数来实现类似效果。这种方式会导致Getters失去缓存能力,每次调用都会重新计算。

getters: {
  // 返回函数,接受参数
  getUserRole: (state) => (roleName) => {
    return state.roles.find(role => role === roleName)
  },
  
  // 检查用户是否有特定权限
  hasPermission: (state) => (permission) => {
    return state.permissions.includes(permission)
  },
  
  // 根据条件过滤数据
  filterPermissions: (state) => (allowed) => {
    return state.permissions.filter(perm => allowed.includes(perm))
  }
}

在组件中使用带参数的Getters:

<template>
  <div>
    <p>是否为管理员:{{ userStore.isAdmin }}</p>
    <p>是否有删除权限:{{ userStore.hasPermission('delete') }}</p>
    <p>是否有编辑权限:{{ userStore.hasPermission('write') }}</p>
    
    <h3>允许的权限:</h3>
    <ul>
      <li v-for="perm in userStore.filterPermissions(['read', 'write'])" :key="perm">
        {{ perm }}
      </li>
    </ul>
  </div>
</template>

Getters的类型推导

Pinia会自动推导Getters的返回类型,在TypeScript项目中,我们可以获得完整的类型提示。

// TypeScript示例
export const useUserStore = defineStore('user', {
  state: () => ({
    age: 30,
    roles: ['user', 'editor'] as const // 使用as const获得更精确的类型
  }),
  
  getters: {
    // 自动推导返回类型为boolean
    isAdult: (state) => state.age >= 18,
    
    // 自动推导返回类型为'user' | 'editor' | undefined
    getUserRole: (state) => (roleName: string) => {
      return state.roles.find(role => role === roleName)
    }
  }
})

8.23.3 StoreToRefs辅助函数

storeToRefs是Pinia提供的一个辅助函数,用于解构Store中的状态和Getters,并保持它们的响应性。

基本用法

import { storeToRefs } from 'pinia'
import { useUserStore } from '../stores/user'

const userStore = useUserStore()

// 解构状态并保持响应性
const { name, age, roles } = storeToRefs(userStore)

// 解构Getters并保持响应性
const { isAdmin, canEdit, userInfo } = storeToRefs(userStore)

// 使用解构后的状态和Getters
console.log(name.value) // 张三
console.log(isAdmin.value) // false
console.log(userInfo.value) // 张三 (30岁)

注意事项

  1. Actions不需要使用storeToRefs:Actions是函数,不需要响应式,所以可以直接解构。

    const { updateUserProfile, resetUser } = userStore
  2. 带参数的Getters不能使用storeToRefs:因为它们返回的是函数,不是响应式对象。

    // 错误:带参数的Getters不能使用storeToRefs
    const { hasPermission } = storeToRefs(userStore) // 这会导致错误
    
    // 正确:直接从Store实例访问
    const { hasPermission } = userStore
  3. storeToRefs只提取响应式属性:只有stategetters会被转换为refactions和Store实例方法不会。

与computed的结合使用

我们可以将storeToRefs与Vue的computed结合使用,创建更复杂的计算属性。

<template>
  <div>
    <h1>{{ displayName }}</h1>
    <p>{{ userStatus }}</p>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '../stores/user'

const userStore = useUserStore()
const { name, age, isAdmin } = storeToRefs(userStore)

// 结合computed创建更复杂的计算属性
const displayName = computed(() => {
  return isAdmin.value ? `${name.value} (管理员)` : name.value
})

const userStatus = computed(() => {
  if (age.value < 18) return '未成年'
  if (isAdmin.value) return '管理员'
  return '普通用户'
})
</script>

最佳实践与注意事项

  1. 状态设计原则

    • 保持状态扁平化,避免过深的嵌套
    • 每个Store负责一个特定的业务领域
    • 状态应该是可序列化的(可以转换为JSON)
    • 避免在状态中存储函数或非序列化数据
  2. Getters使用建议

    • 用于派生状态,避免在组件中重复计算
    • 简单的计算使用箭头函数
    • 需要访问其他Getters的复杂计算使用普通函数
    • 避免在Getters中执行异步操作
  3. 性能优化

    • 避免在Getters中执行昂贵的计算
    • 带参数的Getters会失去缓存,谨慎使用
    • 对于复杂计算,考虑使用computed结合watch进行缓存
  4. TypeScript支持

    • 使用as const获得更精确的类型推导
    • 为复杂状态定义接口
    • 利用Pinia的自动类型推导,减少手动类型注解

小结

本节我们深入学习了Pinia中的State与Getters,包括:

  • 状态的定义和响应式特性
  • 状态修改的多种方式(直接修改、$patch、Actions)
  • Getters的基本使用和高级特性
  • 如何传递参数给Getters
  • storeToRefs辅助函数的使用

合理使用State和Getters可以帮助我们构建清晰、可维护的状态管理系统。State用于存储原始数据,Getters用于派生计算数据,两者结合使用可以提高代码的可读性和复用性。

思考与练习

  1. 设计一个包含嵌套对象和数组的Store状态结构。
  2. 实现不同方式的状态修改,并比较它们的优缺点。
  3. 创建几个Getters,包括简单Getters、访问其他Getters的Getters和带参数的Getters。
  4. 使用storeToRefs解构状态和Getters,并在组件中使用。
  5. 结合computed创建更复杂的计算属性。
  6. 思考如何优化带参数的Getters的性能。
« 上一篇 21-pinia-basics 下一篇 » 23-actions-modules