第75集:Actions异步操作处理

概述

Pinia的Actions是处理异步操作的核心机制,支持同步和异步方法。Actions可以修改State、调用其他Actions,还可以与外部API交互。掌握Actions的异步处理能力对于构建复杂的现代应用至关重要。

核心知识点

1. Actions的基本定义

1.1 Options API风格

在Options API风格中,Actions通过actions选项定义:

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

export interface User {
  id: number
  name: string
  email: string
}

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [] as User[],
    loading: false,
    error: null as string | null
  }),
  
  actions: {
    // 同步action
    addUser(user: User) {
      this.users.push(user)
    },
    
    // 异步action
    async fetchUsers() {
      this.loading = true
      this.error = null
      try {
        const response = await fetch('https://api.example.com/users')
        if (!response.ok) {
          throw new Error('Failed to fetch users')
        }
        const data = await response.json()
        this.users = data
      } catch (err) {
        this.error = err instanceof Error ? err.message : 'An unknown error occurred'
      } finally {
        this.loading = false
      }
    }
  }
})

1.2 Composition API风格

在Composition API风格中,Actions直接定义为普通函数:

// stores/product.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export interface Product {
  id: number
  name: string
  price: number
}

export const useProductStore = defineStore('product', () => {
  const products = ref<Product[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  // 同步action
  function addProduct(product: Product) {
    products.value.push(product)
  }
  
  // 异步action
  async function fetchProducts() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch('https://api.example.com/products')
      if (!response.ok) {
        throw new Error('Failed to fetch products')
      }
      const data = await response.json()
      products.value = data
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'An unknown error occurred'
    } finally {
      loading.value = false
    }
  }
  
  return {
    products,
    loading,
    error,
    addProduct,
    fetchProducts
  }
})

2. Actions的异步处理

2.1 async/await语法

Actions支持使用async/await语法处理异步操作:

// stores/post.ts
export const usePostStore = defineStore('post', {
  state: () => ({
    posts: [],
    loading: false,
    error: null
  }),
  
  actions: {
    async fetchPosts() {
      this.loading = true
      this.error = null
      try {
        // 等待异步操作完成
        const response = await fetch('https://jsonplaceholder.typicode.com/posts')
        const data = await response.json()
        this.posts = data
      } catch (err) {
        this.error = err instanceof Error ? err.message : 'Failed to fetch posts'
      } finally {
        this.loading = false
      }
    },
    
    async fetchPostById(id: number) {
      this.loading = true
      this.error = null
      try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
        const data = await response.json()
        // 查找并更新post
        const index = this.posts.findIndex(post => post.id === id)
        if (index !== -1) {
          this.posts[index] = data
        } else {
          this.posts.push(data)
        }
        return data // 可以返回结果
      } catch (err) {
        this.error = err instanceof Error ? err.message : 'Failed to fetch post'
        throw err // 可以抛出错误
      } finally {
        this.loading = false
      }
    }
  }
})

2.2 Promise链式调用

Actions也支持传统的Promise链式调用:

// stores/comment.ts
export const useCommentStore = defineStore('comment', {
  state: () => ({
    comments: []
  }),
  
  actions: {
    fetchComments(): Promise<void> {
      return fetch('https://jsonplaceholder.typicode.com/comments')
        .then(response => {
          if (!response.ok) {
            throw new Error('Failed to fetch comments')
          }
          return response.json()
        })
        .then(data => {
          this.comments = data
        })
        .catch(err => {
          console.error('Error fetching comments:', err)
        })
    }
  }
})

3. Actions的高级用法

3.1 调用其他Actions

Actions可以调用同一个Store或其他Store的Actions:

// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    users: [],
    profile: null
  }),
  
  actions: {
    async fetchAllData() {
      // 调用同一个Store的其他actions
      await this.fetchUsers()
      await this.fetchProfile()
    },
    
    async fetchUsers() {
      // 实现略
    },
    
    async fetchProfile() {
      // 实现略
    }
  }
})

3.2 调用其他Store的Actions

// stores/cart.ts
import { defineStore } from 'pinia'
import { useProductStore } from './product'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    loading: false
  }),
  
  actions: {
    async addToCart(productId: number, quantity: number = 1) {
      this.loading = true
      try {
        // 调用其他Store的action
        const productStore = useProductStore()
        await productStore.fetchProductById(productId)
        
        // 添加到购物车
        const product = productStore.getProductById(productId)
        if (product) {
          this.items.push({
            productId: product.id,
            name: product.name,
            price: product.price,
            quantity
          })
        }
      } catch (err) {
        console.error('Error adding to cart:', err)
      } finally {
        this.loading = false
      }
    }
  }
})

3.3 Actions的并发处理

可以使用Promise.allPromise.race处理并发异步操作:

// stores/dashboard.ts
export const useDashboardStore = defineStore('dashboard', {
  state: () => ({
    stats: {
      users: 0,
      products: 0,
      orders: 0
    },
    loading: false
  }),
  
  actions: {
    async fetchDashboardData() {
      this.loading = true
      try {
        // 并行获取多个数据
        const [userStats, productStats, orderStats] = await Promise.all([
          this.fetchUserStats(),
          this.fetchProductStats(),
          this.fetchOrderStats()
        ])
        
        this.stats = {
          users: userStats.count,
          products: productStats.count,
          orders: orderStats.count
        }
      } catch (err) {
        console.error('Error fetching dashboard data:', err)
      } finally {
        this.loading = false
      }
    },
    
    async fetchUserStats() {
      // 实现略
      return { count: 100 }
    },
    
    async fetchProductStats() {
      // 实现略
      return { count: 200 }
    },
    
    async fetchOrderStats() {
      // 实现略
      return { count: 300 }
    }
  }
})

3.4 Actions的错误处理

良好的错误处理是Actions设计的重要部分:

// stores/auth.ts
export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: null,
    loading: false,
    error: null
  }),
  
  actions: {
    async login(email: string, password: string) {
      this.loading = true
      this.error = null
      try {
        const response = await fetch('https://api.example.com/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ email, password })
        })
        
        if (!response.ok) {
          const errorData = await response.json()
          throw new Error(errorData.message || 'Login failed')
        }
        
        const data = await response.json()
        this.user = data.user
        this.token = data.token
        
        // 保存token到本地存储
        localStorage.setItem('token', data.token)
        
        return true
      } catch (err) {
        this.error = err instanceof Error ? err.message : 'An unknown error occurred'
        return false
      } finally {
        this.loading = false
      }
    }
  }
})

4. Actions的类型系统

4.1 TypeScript类型推导

Pinia会自动推导Actions的参数和返回类型:

// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    users: [] as User[]
  }),
  
  actions: {
    // 自动推导参数类型为User,返回类型为void
    addUser(user: User) {
      this.users.push(user)
    },
    
    // 自动推导参数类型为number,返回类型为Promise<User | undefined>
    async fetchUserById(id: number) {
      const response = await fetch(`https://api.example.com/users/${id}`)
      return response.json()
    }
  }
})

4.2 显式类型定义

可以为Actions添加显式类型定义:

// stores/product.ts
export const useProductStore = defineStore('product', {
  state: () => ({
    products: [] as Product[]
  }),
  
  actions: {
    // 显式类型定义
    addProduct: function(user: User): void {
      this.users.push(user)
    },
    
    // 显式异步返回类型
    fetchProductById: async function(id: number): Promise<User | null> {
      try {
        const response = await fetch(`https://api.example.com/users/${id}`)
        return response.json()
      } catch (err) {
        return null
      }
    }
  }
})

5. Actions的性能优化

5.1 防抖与节流

对于频繁调用的Actions,可以使用防抖或节流优化:

// stores/search.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useSearchStore = defineStore('search', () => {
  const results = ref([])
  const loading = ref(false)
  let debounceTimer: number | null = null
  
  // 防抖搜索
  function search(query: string) {
    if (debounceTimer) {
      clearTimeout(debounceTimer)
    }
    
    debounceTimer = window.setTimeout(async () => {
      loading.value = true
      try {
        const response = await fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`)
        const data = await response.json()
        results.value = data
      } catch (err) {
        console.error('Search error:', err)
      } finally {
        loading.value = false
      }
    }, 300)
  }
  
  return {
    results,
    loading,
    search
  }
})

5.2 取消请求

可以使用AbortController取消正在进行的请求:

// stores/product.ts
export const useProductStore = defineStore('product', {
  state: () => ({
    products: [],
    loading: false
  }),
  
  actions: {
    fetchProducts() {
      // 创建AbortController
      const controller = new AbortController()
      const signal = controller.signal
      
      this.loading = true
      
      fetch('https://api.example.com/products', { signal })
        .then(response => response.json())
        .then(data => {
          this.products = data
        })
        .catch(err => {
          if (err.name === 'AbortError') {
            console.log('Fetch aborted')
            return
          }
          console.error('Error fetching products:', err)
        })
        .finally(() => {
          this.loading = false
        })
      
      // 保存controller,以便后续取消
      this._fetchController = controller
    },
    
    cancelFetch() {
      if (this._fetchController) {
        this._fetchController.abort()
        this._fetchController = null
      }
    }
  }
})

最佳实践

1. Actions设计原则

  • 单一职责:每个Action只负责一个业务逻辑
  • 异步优先:对于涉及API调用的操作,使用异步Actions
  • 错误处理:所有异步Actions都应该有适当的错误处理
  • 状态管理:在Actions中管理loading和error状态
  • 返回结果:异步Actions应该返回适当的结果或抛出错误

2. Actions命名规范

  • 使用动词开头,如fetchUsersaddProduct
  • 对于异步操作,使用fetchloadsave等前缀
  • 对于破坏性操作,使用deleteremoveclear等前缀
  • 使用驼峰命名法,如fetchUserById而不是fetch_user_by_id

3. 异步流程管理

  • 使用async/await语法,提高代码可读性
  • 使用try/catch/finally处理异步错误
  • 合理使用Promise.all处理并发操作
  • 考虑使用状态管理库(如Pinia)管理异步流程

4. 性能优化

  • 对频繁调用的Actions使用防抖或节流
  • 实现请求取消机制,避免不必要的网络请求
  • 合理使用缓存,避免重复请求
  • 考虑使用批量更新,减少状态更新次数

常见问题与解决方案

1. Actions中无法访问this

问题:在箭头函数定义的Actions中无法访问this

解决方案

  • 使用普通函数定义Actions,而不是箭头函数
  • 在Composition API风格中,直接访问状态变量
// ❌ 错误:箭头函数无法访问this
const useStore = defineStore('store', {
  actions: {
    fetchData: () => {
      this.loading = true // 无法访问this
    }
  }
})

// ✅ 正确:使用普通函数
const useStore = defineStore('store', {
  actions: {
    fetchData() {
      this.loading = true // 可以访问this
    }
  }
})

2. 异步Actions的错误处理

问题:异步Actions的错误没有被正确捕获。

解决方案

  • 使用try/catch包裹异步操作
  • 在组件中捕获Actions抛出的错误
// store中
async fetchData() {
  try {
    const response = await fetch('https://api.example.com')
    return response.json()
  } catch (err) {
    this.error = err instanceof Error ? err.message : 'An unknown error occurred'
    throw err // 重新抛出错误,让组件可以捕获
  }
}

// 组件中
async function loadData() {
  try {
    await store.fetchData()
  } catch (err) {
    console.error('Failed to load data:', err)
  }
}

3. Actions的并发控制

问题:多次快速调用同一个异步Action导致并发问题。

解决方案

  • 使用防抖或节流
  • 实现请求锁机制
// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    users: [],
    loading: false
  }),
  
  actions: {
    async fetchUsers() {
      // 如果已经在加载中,直接返回
      if (this.loading) {
        return
      }
      
      this.loading = true
      try {
        // 实现略
      } finally {
        this.loading = false
      }
    }
  }
})

4. Actions的测试

问题:异步Actions难以测试。

解决方案

  • 使用测试框架如Vitest或Jest
  • 模拟API请求
  • 测试Actions的各种状态
// tests/userStore.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useUserStore } from '../stores/user'
import { setActivePinia, createPinia } from 'pinia'

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('should fetch users successfully', async () => {
    // 模拟fetch
    vi.spyOn(global, 'fetch').mockResolvedValue({
      ok: true,
      json: () => Promise.resolve([{ id: 1, name: 'John Doe' }])
    } as Response)
    
    const store = useUserStore()
    await store.fetchUsers()
    
    expect(store.users).toHaveLength(1)
    expect(store.loading).toBe(false)
    expect(store.error).toBe(null)
  })
})

进一步学习资源

  1. Pinia官方文档 - Actions
  2. JavaScript Async/Await
  3. Promise API
  4. AbortController API
  5. TypeScript异步编程

课后练习

  1. 基础练习

    • 创建一个产品管理Store,包含产品列表
    • 实现异步Actions:fetchProductsfetchProductByIdcreateProduct
    • 实现loading和error状态管理
    • 在组件中使用这些Actions
  2. 进阶练习

    • 创建一个用户认证Store
    • 实现异步Actions:loginlogoutregister
    • 实现token管理和本地存储
    • 实现请求取消机制
  3. 并发处理练习

    • 创建一个仪表盘Store
    • 实现并行获取多个数据的Action
    • 使用Promise.allPromise.race处理并发
    • 测试不同并发策略的性能
  4. 错误处理练习

    • 实现全面的错误处理机制
    • 测试各种错误场景
    • 实现错误重试机制
    • 实现用户友好的错误提示

通过本节课的学习,你应该能够掌握Pinia中Actions的异步处理能力,理解Actions的高级用法,掌握Actions的类型系统,以及Actions的性能优化策略。这些知识将帮助你构建复杂、高效、可维护的现代应用。

« 上一篇 Getters计算属性进阶 - Pinia高级状态派生 下一篇 » Store间通信与组合 - Pinia状态管理架构