Skip to content
On this page

Pinia State管理

State是Pinia Store的核心组成部分,用于存储应用的数据。本章将详细介绍如何有效地管理和操作State,包括响应式、持久化、批量更新等高级主题。

State基础概念

State定义

在Pinia中,State是一个返回初始状态的函数,这确保了每个组件实例都能获得状态的独立副本。

typescript
import { defineStore } from 'pinia'

// 基础State定义
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Eduardo',
    isAdmin: false
  })
})

State类型声明

typescript
// 使用TypeScript进行类型声明
interface UserInfo {
  id: string
  name: string
  email: string
  avatar: string
}

interface Preferences {
  theme: 'light' | 'dark'
  language: string
  notifications: boolean
}

export const useUserStore = defineStore('user', {
  state: (): { 
    userInfo: UserInfo | null, 
    preferences: Preferences,
    loading: boolean 
  } => ({
    userInfo: null,
    preferences: {
      theme: 'light',
      language: 'en',
      notifications: true
    },
    loading: false
  })
})

State响应式原理

Vue响应式系统集成

Pinia的State完全集成Vue的响应式系统,这意味着当State改变时,使用这些状态的组件会自动更新。

typescript
import { defineStore } from 'pinia'

export const useTodosStore = defineStore('todos', {
  state: () => ({
    todos: [] as Todo[],
    filter: 'all' as 'all' | 'active' | 'completed'
  }),
  
  getters: {
    filteredTodos: (state) => {
      switch (state.filter) {
        case 'active':
          return state.todos.filter(todo => !todo.completed)
        case 'completed':
          return state.todos.filter(todo => todo.completed)
        default:
          return state.todos
      }
    },
    
    activeCount: (state) => {
      return state.todos.filter(todo => !todo.completed).length
    }
  },
  
  actions: {
    addTodo(text: string) {
      const newTodo: Todo = {
        id: Date.now().toString(),
        text,
        completed: false
      }
      this.todos.push(newTodo) // 响应式更新
    },
    
    toggleTodo(id: string) {
      const todo = this.todos.find(todo => todo.id === id)
      if (todo) {
        todo.completed = !todo.completed // 响应式更新
      }
    }
  }
})

interface Todo {
  id: string
  text: string
  completed: boolean
}

在组件中使用响应式State

vue
<template>
  <div>
    <h2>Todos ({{ todosStore.activeCount }} active)</h2>
    
    <!-- 添加新任务 -->
    <form @submit.prevent="addTodo">
      <input 
        v-model="newTodo" 
        placeholder="Add new todo..." 
        :disabled="todosStore.loading"
      />
      <button type="submit" :disabled="!newTodo || todosStore.loading">
        Add
      </button>
    </form>
    
    <!-- 过滤器 -->
    <div class="filters">
      <button 
        @click="setFilter('all')" 
        :class="{ active: todosStore.filter === 'all' }"
      >
        All
      </button>
      <button 
        @click="setFilter('active')" 
        :class="{ active: todosStore.filter === 'active' }"
      >
        Active
      </button>
      <button 
        @click="setFilter('completed')" 
        :class="{ active: todosStore.filter === 'completed' }"
      >
        Completed
      </button>
    </div>
    
    <!-- 任务列表 -->
    <ul>
      <li 
        v-for="todo in todosStore.filteredTodos" 
        :key="todo.id"
        :class="{ completed: todo.completed }"
      >
        <input 
          type="checkbox" 
          :checked="todo.completed"
          @change="toggleTodo(todo.id)"
        />
        <span>{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">Remove</button>
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useTodosStore } from '@/stores/todos'

const todosStore = useTodosStore()
const newTodo = ref('')

const addTodo = () => {
  if (newTodo.value.trim()) {
    todosStore.addTodo(newTodo.value.trim())
    newTodo.value = ''
  }
}

const toggleTodo = (id: string) => {
  todosStore.toggleTodo(id)
}

const removeTodo = (id: string) => {
  todosStore.todos = todosStore.todos.filter(todo => todo.id !== id)
}

const setFilter = (filter: 'all' | 'active' | 'completed') => {
  todosStore.filter = filter
}
</script>

<style scoped>
.completed {
  text-decoration: line-through;
  opacity: 0.6;
}

.filters button {
  margin-right: 8px;
}

.filters button.active {
  font-weight: bold;
  color: blue;
}
</style>

State修改方式

直接修改State

在Pinia的Actions中,可以直接修改State:

typescript
export const useProfileStore = defineStore('profile', {
  state: () => ({
    profile: {
      name: '',
      email: '',
      bio: '',
      avatar: ''
    } as UserProfile,
    editing: false
  }),
  
  actions: {
    // 直接修改整个对象
    setProfile(profile: UserProfile) {
      this.profile = profile
    },
    
    // 直接修改属性
    setName(name: string) {
      this.profile.name = name
    },
    
    setEmail(email: string) {
      this.profile.email = email
    },
    
    setBio(bio: string) {
      this.profile.bio = bio
    },
    
    // 批量更新
    updateProfile(updates: Partial<UserProfile>) {
      this.profile = { ...this.profile, ...updates }
    },
    
    // 使用Object.assign
    assignProfile(updates: Partial<UserProfile>) {
      Object.assign(this.profile, updates)
    },
    
    // 开始编辑
    startEdit() {
      this.editing = true
    },
    
    // 完成编辑
    finishEdit() {
      this.editing = false
    }
  }
})

interface UserProfile {
  name: string
  email: string
  bio: string
  avatar: string
}

使用$patch方法

$patch方法允许你批量更新State,这对于复杂的更新操作非常有用:

typescript
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as CartItem[],
    total: 0,
    discount: 0,
    shipping: 0,
    tax: 0,
    currency: 'USD' as Currency
  }),
  
  actions: {
    // 使用对象进行批量更新
    updateCart(updates: Partial<CartState>) {
      this.$patch(updates)
    },
    
    // 使用函数进行复杂的批量更新
    applyDiscountAndShipping(discount: number, shipping: number) {
      this.$patch((state) => {
        state.discount = discount
        state.shipping = shipping
        state.total = this.calculateTotal()
      })
    },
    
    // 添加商品到购物车
    addToCart(item: CartItem) {
      const existingItem = this.items.find(i => i.id === item.id)
      if (existingItem) {
        this.$patch((state) => {
          existingItem.quantity += item.quantity
          state.total = this.calculateTotal()
        })
      } else {
        this.$patch((state) => {
          state.items.push(item)
          state.total = this.calculateTotal()
        })
      }
    },
    
    // 更新商品数量
    updateItemQuantity(id: string, quantity: number) {
      if (quantity <= 0) {
        this.removeItem(id)
        return
      }
      
      this.$patch((state) => {
        const item = state.items.find(i => i.id === id)
        if (item) {
          item.quantity = quantity
          state.total = this.calculateTotal()
        }
      })
    },
    
    // 移除商品
    removeItem(id: string) {
      this.$patch((state) => {
        state.items = state.items.filter(item => item.id !== id)
        state.total = this.calculateTotal()
      })
    },
    
    // 清空购物车
    clearCart() {
      this.$patch({
        items: [],
        total: 0,
        discount: 0
      })
    },
    
    // 内部辅助方法
    calculateTotal(): number {
      const subtotal = this.items.reduce(
        (sum, item) => sum + item.price * item.quantity, 
        0
      )
      return Math.max(0, subtotal - this.discount + this.shipping + this.tax)
    }
  }
})

interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

type Currency = 'USD' | 'EUR' | 'CNY'

type CartState = ReturnType<typeof useCartStore>['$state']

State持久化

使用pinia-plugin-persistedstate

首先安装持久化插件:

bash
npm install pinia-plugin-persistedstate

然后在main.js中配置:

javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

pinia.use(piniaPluginPersistedstate)

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

基础持久化配置

typescript
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    id: null as string | null,
    name: '',
    email: '',
    preferences: {
      theme: 'light' as 'light' | 'dark',
      language: 'en',
      notifications: true
    },
    lastLogin: null as string | null
  }),
  
  // 启用持久化
  persist: true
})

高级持久化配置

typescript
export const useSettingsStore = defineStore('settings', {
  state: () => ({
    general: {
      theme: 'light' as 'light' | 'dark',
      language: 'en',
      dateFormat: 'MM/DD/YYYY'
    },
    notifications: {
      email: true,
      push: false,
      sound: true
    },
    privacy: {
      shareData: false,
      marketing: false
    },
    advanced: {
      autoSave: true,
      offlineMode: false,
      experimentalFeatures: false
    }
  }),
  
  persist: [
    // 持久化整个store
    {
      key: 'app_settings',
      storage: localStorage,
      // 只持久化general和notifications部分
      pick: ['general', 'notifications']
    },
    // 只持久化privacy部分,使用sessionStorage
    {
      key: 'privacy_settings',
      storage: sessionStorage,
      pick: ['privacy']
    }
  ],
  
  actions: {
    updateGeneralSettings(settings: Partial<Settings['general']>) {
      this.general = { ...this.general, ...settings }
    },
    
    updateNotificationSettings(settings: Partial<Settings['notifications']>) {
      this.notifications = { ...this.notifications, ...settings }
    },
    
    resetToDefaults() {
      this.$patch({
        general: {
          theme: 'light',
          language: 'en',
          dateFormat: 'MM/DD/YYYY'
        },
        notifications: {
          email: true,
          push: false,
          sound: true
        },
        privacy: {
          shareData: false,
          marketing: false
        }
      })
    }
  }
})

interface Settings {
  general: {
    theme: 'light' | 'dark'
    language: string
    dateFormat: string
  }
  notifications: {
    email: boolean
    push: boolean
    sound: boolean
  }
  privacy: {
    shareData: boolean
    marketing: boolean
  }
  advanced: {
    autoSave: boolean
    offlineMode: boolean
    experimentalFeatures: boolean
  }
}

自定义持久化逻辑

typescript
export const useSessionStore = defineStore('session', {
  state: () => ({
    userId: null as string | null,
    sessionId: null as string | null,
    expiresAt: null as number | null,
    permissions: [] as string[],
    lastActivity: Date.now()
  }),
  
  persist: {
    key: 'user_session',
    storage: localStorage,
    serializer: {
      // 自定义序列化方法
      serialize: (state) => {
        // 在序列化前处理敏感数据
        const serialized = { ...state }
        // 不序列化某些敏感字段
        if (serialized.sessionId) {
          // 可以对敏感数据进行加密
          serialized.sessionId = btoa(serialized.sessionId) // 简单示例
        }
        return JSON.stringify(serialized)
      },
      
      // 自定义反序列化方法
      deserialize: (value) => {
        const state = JSON.parse(value)
        // 在反序列化后处理数据
        if (state.sessionId) {
          state.sessionId = atob(state.sessionId) // 简单解密示例
        }
        return state
      }
    },
    
    // 自定义存储逻辑
    write: (state, storage, key) => {
      // 检查会话是否过期
      if (state.expiresAt && Date.now() > state.expiresAt) {
        // 会话过期,清除存储
        storage.removeItem(key)
        return
      }
      
      // 更新最后活动时间
      state.lastActivity = Date.now()
      storage.setItem(key, JSON.stringify(state))
    },
    
    // 自定义读取逻辑
    read: (storage, key) => {
      const value = storage.getItem(key)
      if (!value) return undefined
      
      const state = JSON.parse(value)
      
      // 检查会话是否过期
      if (state.expiresAt && Date.now() > state.expiresAt) {
        storage.removeItem(key)
        return undefined
      }
      
      return state
    }
  },
  
  getters: {
    isExpired: (state) => {
      return state.expiresAt ? Date.now() > state.expiresAt : true
    },
    
    isValid: (state) => {
      return !state.isExpired && state.sessionId !== null
    }
  },
  
  actions: {
    createSession(userId: string, permissions: string[] = []) {
      this.userId = userId
      this.sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
      this.expiresAt = Date.now() + 24 * 60 * 60 * 1000 // 24小时后过期
      this.permissions = permissions
      this.lastActivity = Date.now()
    },
    
    extendSession() {
      if (this.sessionId) {
        this.expiresAt = Date.now() + 24 * 60 * 60 * 1000 // 延长24小时
        this.lastActivity = Date.now()
      }
    },
    
    clearSession() {
      this.userId = null
      this.sessionId = null
      this.expiresAt = null
      this.permissions = []
    }
  }
})

State变更监听

使用Store订阅

typescript
import { watch } from 'vue'

export const useLoggerStore = defineStore('logger', {
  state: () => ({
    logs: [] as LogEntry[],
    maxLogs: 1000
  }),
  
  actions: {
    addLog(level: LogLevel, message: string, data?: any) {
      const logEntry: LogEntry = {
        id: Date.now().toString(),
        timestamp: new Date().toISOString(),
        level,
        message,
        data
      }
      
      this.logs.push(logEntry)
      
      // 限制日志数量
      if (this.logs.length > this.maxLogs) {
        this.logs = this.logs.slice(-this.maxLogs)
      }
    },
    
    clearLogs() {
      this.logs = []
    },
    
    error(message: string, data?: any) {
      this.addLog('error', message, data)
    },
    
    warn(message: string, data?: any) {
      this.addLog('warn', message, data)
    },
    
    info(message: string, data?: any) {
      this.addLog('info', message, data)
    }
  }
})

// 在Store定义之外设置监听
export function setupStoreListeners() {
  const loggerStore = useLoggerStore()
  
  // 监听状态变化
  const counterStore = useCounterStore()
  const unsubscribe = counterStore.$subscribe((mutation, state) => {
    loggerStore.info(`Counter changed: ${mutation.storeId}.${mutation.type}`, {
      newValue: state.count,
      mutation
    })
  }, {
    detached: true // 不受组件卸载影响
  })
  
  // 监听Store的任何变化
  const userStore = useUserStore()
  userStore.$subscribe((mutation, state) => {
    if (mutation.type === 'direct') {
      loggerStore.info(`User state changed`, {
        mutation,
        state
      })
    }
  })
  
  return unsubscribe
}

type LogLevel = 'info' | 'warn' | 'error'

interface LogEntry {
  id: string
  timestamp: string
  level: LogLevel
  message: string
  data?: any
}

深度监听State

typescript
export const useDeepWatchStore = defineStore('deepWatch', {
  state: () => ({
    complexData: {
      users: [] as User[],
      settings: {
        filters: {
          age: { min: 0, max: 100 },
          department: [] as string[],
          active: true
        },
        sorting: {
          field: 'name',
          order: 'asc' as 'asc' | 'desc'
        }
      },
      metadata: {
        lastUpdated: new Date(),
        version: '1.0.0'
      }
    }
  }),
  
  actions: {
    // 初始化监听器
    initWatchers() {
      // 监听特定嵌套属性的变化
      this.$subscribe((mutation, state) => {
        // 检查是否是filters的变更
        if (mutation.type === 'direct' && 
            (mutation.events as any).key?.includes('filters')) {
          this.handleFiltersChange()
        }
      })
    },
    
    handleFiltersChange() {
      console.log('Filters changed, updating user list...')
      // 根据新的过滤器重新计算用户列表
      this.applyFilters()
    },
    
    applyFilters() {
      // 应用过滤器逻辑
      const { min, max } = this.complexData.settings.filters.age
      const departments = this.complexData.settings.filters.department
      const activeOnly = this.complexData.settings.filters.active
      
      // 这里实现具体的过滤逻辑
      console.log('Applied filters:', { min, max, departments, activeOnly })
    },
    
    updateFilterAge(min: number, max: number) {
      this.complexData.settings.filters.age = { min, max }
    },
    
    updateFilterDepartments(departments: string[]) {
      this.complexData.settings.filters.department = departments
    },
    
    updateFilterActive(active: boolean) {
      this.complexData.settings.filters.active = active
    }
  }
})

interface User {
  id: string
  name: string
  age: number
  department: string
  active: boolean
}

State性能优化

避免不必要的重渲染

typescript
export const useOptimizedStore = defineStore('optimized', {
  state: () => ({
    // 大量数据
    largeDataset: [] as DataItem[],
    // 计算结果缓存
    computedResults: new Map<string, ComputedResult>(),
    // UI状态(不需要响应式)
    uiState: {
      sidebarOpen: false,
      modalVisible: false,
      currentTab: 'overview'
    }
  }),
  
  getters: {
    // 使用computed包装昂贵的计算
    expensiveCalculation: (state) => {
      return computed(() => {
        // 这里的计算只在依赖变化时重新执行
        return state.largeDataset
          .filter(item => item.active)
          .reduce((sum, item) => sum + item.value, 0)
      })
    },
    
    // 分页数据
    getPaginatedData: (state) => {
      return (page: number, pageSize: number) => {
        const start = (page - 1) * pageSize
        return state.largeDataset.slice(start, start + pageSize)
      }
    }
  },
  
  actions: {
    // 批量更新大数据集
    batchUpdateData(updates: DataUpdate[]) {
      // 使用$patch进行批量更新,减少重渲染次数
      this.$patch((state) => {
        for (const update of updates) {
          const item = state.largeDataset.find(d => d.id === update.id)
          if (item) {
            Object.assign(item, update.changes)
          }
        }
      })
    },
    
    // 更新UI状态(非响应式)
    updateUiState(newState: Partial<UiState>) {
      // 对于纯UI状态,可以考虑不使用响应式
      // 但这取决于具体需求
      this.uiState = { ...this.uiState, ...newState }
    },
    
    // 清理缓存
    clearComputedCache() {
      this.computedResults.clear()
    }
  }
})

interface DataItem {
  id: string
  value: number
  active: boolean
  category: string
}

interface ComputedResult {
  id: string
  result: any
  timestamp: number
}

interface DataUpdate {
  id: string
  changes: Partial<DataItem>
}

interface UiState {
  sidebarOpen: boolean
  modalVisible: boolean
  currentTab: string
}

使用可选的响应式

typescript
import { shallowRef, triggerRef } from 'vue'

export const useShallowStore = defineStore('shallow', {
  state: () => ({
    // 使用浅层响应式存储大型对象
    largeObject: shallowRef({} as LargeObject),
    // 普通响应式数据
    metadata: {
      loaded: false,
      lastUpdate: null as number | null
    }
  }),
  
  actions: {
    // 更新大型对象而不触发深层响应
    updateLargeObject(partialUpdate: Partial<LargeObject>) {
      const current = this.largeObject.value
      this.largeObject.value = { ...current, ...partialUpdate }
      
      // 如果需要强制更新,使用triggerRef
      // triggerRef(this.largeObject)
    },
    
    // 批量更新大型对象
    batchUpdateLargeObject(updates: Array<Partial<LargeObject>>) {
      let current = this.largeObject.value
      for (const update of updates) {
        current = { ...current, ...update }
      }
      this.largeObject.value = current
    },
    
    markAsLoaded() {
      this.metadata.loaded = true
      this.metadata.lastUpdate = Date.now()
    }
  }
})

interface LargeObject {
  data: any[]
  settings: Record<string, any>
  cache: Map<string, any>
  // ... 其他大型数据结构
}

State测试

State单元测试

typescript
// stores/__tests__/counter.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../counter'

describe('Counter Store', () => {
  beforeEach(() => {
    // 创建新的pinia实例
    setActivePinia(createPinia())
  })

  it('should initialize with default count of 0', () => {
    const counter = useCounterStore()
    expect(counter.count).toBe(0)
  })

  it('should increment count', () => {
    const counter = useCounterStore()
    counter.increment()
    expect(counter.count).toBe(1)
  })

  it('should decrement count', () => {
    const counter = useCounterStore()
    counter.count = 5
    counter.decrement()
    expect(counter.count).toBe(4)
  })

  it('should compute double count', () => {
    const counter = useCounterStore()
    counter.count = 4
    expect(counter.doubleCount).toBe(8)
  })

  it('should handle async increment', async () => {
    const counter = useCounterStore()
    await counter.incrementAsync()
    expect(counter.count).toBe(1)
  })
})

// stores/__tests__/user.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '../user'
import { vi, describe, it, beforeEach, expect } from 'vitest'

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('should handle user login', async () => {
    const userStore = useUserStore()
    
    // Mock API response
    global.fetch = vi.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve({
          id: '123',
          name: 'John Doe',
          email: 'john@example.com'
        })
      } as Response)
    )

    await userStore.login({ email: 'test@example.com', password: 'password' })

    expect(userStore.user).toEqual({
      id: '123',
      name: 'John Doe',
      email: 'john@example.com'
    })
    expect(userStore.isAuthenticated).toBe(true)
  })

  it('should handle login error', async () => {
    const userStore = useUserStore()
    
    // Mock API error response
    global.fetch = vi.fn(() =>
      Promise.resolve({
        ok: false,
        status: 401
      } as Response)
    )

    await expect(
      userStore.login({ email: 'test@example.com', password: 'wrong' })
    ).rejects.toThrow()

    expect(userStore.error).toBeDefined()
  })
})

最佳实践

1. State结构设计

typescript
// 推荐:扁平化状态结构
export const useRecommendedStore = defineStore('recommended', {
  state: () => ({
    // 将相关数据组织在一起
    user: {
      profile: null as UserProfile | null,
      preferences: {} as UserPreferences,
      sessions: [] as Session[]
    },
    
    // 分离UI状态和业务状态
    ui: {
      loading: false,
      error: null as string | null,
      notifications: [] as Notification[]
    },
    
    // 使用ID引用关联数据
    entities: {
      posts: {} as Record<string, Post>,
      comments: {} as Record<string, Comment>
    },
    relations: {
      userPosts: {} as Record<string, string[]> // userId -> postIds
    }
  })
})

// 避免:过度嵌套的状态结构
export const useAvoidStore = defineStore('avoid', {
  state: () => ({
    complexNestedData: {
      level1: {
        level2: {
          level3: {
            actualData: 'hard to access'
          }
        }
      }
    }
  })
})

2. State初始化

typescript
export const useInitializedStore = defineStore('initialized', {
  state: () => ({
    // 从环境变量或配置初始化
    apiUrl: process.env.VUE_APP_API_URL || 'https://api.example.com',
    
    // 从本地存储初始化
    theme: localStorage.getItem('theme') as 'light' | 'dark' || 'light',
    
    // 从URL参数初始化
    locale: new URLSearchParams(window.location.search).get('lang') || 'en',
    
    // 从设备特性初始化
    prefersDark: window.matchMedia('(prefers-color-scheme: dark)').matches
  }),
  
  actions: {
    initializeFromStorage() {
      const storedTheme = localStorage.getItem('user-theme')
      if (storedTheme) {
        this.theme = storedTheme as 'light' | 'dark'
      }
    }
  }
})

3. 状态验证

typescript
export const useValidatedStore = defineStore('validated', {
  state: () => ({
    email: '',
    age: 0,
    preferences: {
      theme: 'light' as 'light' | 'dark',
      newsletter: false
    }
  }),
  
  actions: {
    setEmail(newEmail: string) {
      if (!this.isValidEmail(newEmail)) {
        throw new Error('Invalid email format')
      }
      this.email = newEmail
    },
    
    setAge(newAge: number) {
      if (newAge < 0 || newAge > 150) {
        throw new Error('Age must be between 0 and 150')
      }
      this.age = newAge
    },
    
    private isValidEmail(email: string): boolean {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      return emailRegex.test(email)
    }
  }
})

总结

State管理是Pinia的核心功能,通过本章的学习,你应该掌握:

  1. State的基础定义和类型声明
  2. 响应式系统的集成和使用
  3. 多种State修改方式:直接修改和$patch
  4. State持久化的实现
  5. State变更监听机制
  6. 性能优化技巧
  7. State测试方法
  8. 最佳实践原则

有效的State管理是构建可维护、高性能Vue应用的关键,确保遵循这些原则来组织你的应用状态。