第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.all或Promise.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命名规范
- 使用动词开头,如
fetchUsers、addProduct - 对于异步操作,使用
fetch、load、save等前缀 - 对于破坏性操作,使用
delete、remove、clear等前缀 - 使用驼峰命名法,如
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)
})
})进一步学习资源
课后练习
基础练习:
- 创建一个产品管理Store,包含产品列表
- 实现异步Actions:
fetchProducts、fetchProductById、createProduct - 实现loading和error状态管理
- 在组件中使用这些Actions
进阶练习:
- 创建一个用户认证Store
- 实现异步Actions:
login、logout、register - 实现token管理和本地存储
- 实现请求取消机制
并发处理练习:
- 创建一个仪表盘Store
- 实现并行获取多个数据的Action
- 使用
Promise.all和Promise.race处理并发 - 测试不同并发策略的性能
错误处理练习:
- 实现全面的错误处理机制
- 测试各种错误场景
- 实现错误重试机制
- 实现用户友好的错误提示
通过本节课的学习,你应该能够掌握Pinia中Actions的异步处理能力,理解Actions的高级用法,掌握Actions的类型系统,以及Actions的性能优化策略。这些知识将帮助你构建复杂、高效、可维护的现代应用。