第三章:掌握 actions 与方法系统
在前两章中,我们深入探讨了 Pinia 的响应式状态实现机制。现在,让我们聚焦于 Pinia 中的 actions 系统,理解其 this 绑定机制、异步处理以及 $onAction 钩子的实现原理。
actions 的本质:定义在 setup() 内部的函数
在 Pinia 中,actions 就是在 setup() 函数内部定义的普通函数。这些函数通过代理机制挂载到 store 实例上,使得它们可以访问 store 的状态和其他方法:
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
// 这些就是 actions
function increment() {
count.value++
}
function incrementBy(payload) {
count.value += payload.amount
}
async function incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
count.value++
}
return { count, increment, incrementBy, incrementAsync }
})与 Vuex 不同,Pinia 中的 actions 不需要通过 commit 或 dispatch 调用,而是直接作为 store 的方法调用:
const counter = useCounterStore()
counter.increment() // 直接调用
counter.incrementBy({ amount: 5 }) // 带参数调用
counter.incrementAsync() // 异步调用this 绑定机制:在 defineStore 内部,actions 函数被 bind(store)
Pinia 中 actions 的一个重要特性是 this 关键字始终指向 store 实例。这是通过在 defineStore 内部将 actions 函数绑定到 store 实例实现的:
// 简化的 this 绑定实现原理
function bindActions(store, actions) {
const boundActions = {}
for (const [name, action] of Object.entries(actions)) {
// 将 action 绑定到 store 实例
boundActions[name] = action.bind(store)
}
return boundActions
}
// 在 defineStore 中的处理过程
function defineStore(id, setup) {
return function useStore() {
// 执行 setup 函数
const setupResult = setup()
// 分离状态和 actions
const state = {}
const actions = {}
for (const [key, value] of Object.entries(setupResult)) {
if (typeof value === 'function') {
actions[key] = value
} else {
state[key] = value
}
}
// 创建 store 实例
const store = reactive({
...state,
...bindActions(store, actions) // 绑定 actions
})
return store
}
}这种绑定机制使得在 actions 中可以通过 this 访问 store 的任何属性:
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const permissions = ref([])
// 在 action 中使用 this 访问状态
function login(credentials) {
// 可以直接访问 this.user 和 this.permissions
this.user = { name: credentials.username }
this.permissions = ['read']
}
// 也可以调用其他 actions
function logout() {
this.user = null
this.permissions = []
console.log('User logged out')
}
// 在 action 中调用其他 action
function resetAndLogout() {
this.$reset() // 调用内置方法
this.logout() // 调用其他 action
}
return { user, permissions, login, logout, resetAndLogout }
})为什么 actions 能访问 this?—— this 被绑定为 store 实例
这种设计使得 Pinia 的 actions 更像类的方法,提供了更加直观的 API。开发者不需要记住复杂的命名空间或调用方式,直接通过 this 就能访问所有 store 属性。
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const coupon = ref(null)
// 计算属性也可以通过 this 访问
const total = computed(() => {
let sum = this.items.reduce((total, item) => total + item.price, 0)
if (this.coupon) {
sum -= this.coupon.discount
}
return sum
})
function addItem(item) {
// 通过 this 访问状态
this.items.push(item)
}
function applyCoupon(code) {
// 模拟异步操作
api.validateCoupon(code).then(validCoupon => {
// 在异步回调中仍然可以使用 this
this.coupon = validCoupon
})
}
return { items, coupon, total, addItem, applyCoupon }
})避免箭头函数(无 this)导致无法访问 state
需要注意的是,如果使用箭头函数定义 actions,将无法访问 this,因为箭头函数没有自己的 this 上下文:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
// 正确:普通函数,this 指向 store
function increment() {
this.count++ // 正常工作
}
// 错误:箭头函数,this 不指向 store
const decrement = () => {
this.count-- // this 是 undefined,会报错
}
// 正确:箭头函数显式接收状态
const decrement = (state) => {
state.count-- // 通过参数访问状态
}
return { count, increment, decrement }
})异步 actions:async/await 自然支持,返回 Promise
Pinia 对异步操作提供了原生支持,actions 可以直接使用 async/await 语法:
export const useApiStore = defineStore('api', () => {
const users = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchUsers() {
this.loading = true
this.error = null
try {
// 直接使用 await
const response = await fetch('/api/users')
this.users = await response.json()
} catch (err) {
this.error = err.message
throw err // 可以继续抛出错误
} finally {
this.loading = false
}
}
async function createUser(userData) {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
const newUser = await response.json()
this.users.push(newUser)
return newUser
} catch (err) {
this.error = err.message
throw err
}
}
return { users, loading, error, fetchUsers, createUser }
})
// 在组件中使用
async function handleCreateUser() {
try {
// 可以直接 await actions
const newUser = await apiStore.createUser({ name: 'John' })
console.log('Created user:', newUser)
} catch (error) {
console.error('Failed to create user:', error)
}
}$onAction 钩子:如何拦截 action 的执行、成功、失败?
Pinia 提供了 $onAction 钩子,允许我们在 actions 执行前后进行拦截和处理,这对于日志记录、性能监控、错误处理等场景非常有用:
// 简化的 $onAction 实现原理
function createOnAction(store) {
const subscriptions = []
function $onAction(callback) {
subscriptions.push(callback)
// 返回取消订阅函数
return () => {
const index = subscriptions.indexOf(callback)
if (index > -1) {
subscriptions.splice(index, 1)
}
}
}
// 在 actions 包装函数中触发钩子
function wrapAction(name, action) {
return function (...args) {
const after = [] // 成功回调
const onError = [] // 错误回调
// 触发订阅回调
subscriptions.forEach(callback => {
callback({
name,
store,
args,
after: (callback) => after.push(callback),
onError: (callback) => onError.push(callback)
})
})
let result
try {
// 执行 action
result = action.apply(this, args)
// 处理异步结果
if (result instanceof Promise) {
return result.then(value => {
// 执行成功回调
after.forEach(cb => cb(value))
return value
}).catch(error => {
// 执行错误回调
onError.forEach(cb => cb(error))
throw error
})
} else {
// 执行成功回调
after.forEach(cb => cb(result))
return result
}
} catch (error) {
// 执行错误回调
onError.forEach(cb => cb(error))
throw error
}
}
}
return $onAction
}使用 $onAction 进行日志记录和监控:
const store = useUserStore()
// 订阅所有 actions
const unsubscribe = store.$onAction((context) => {
console.log('Action started:', {
name: context.name,
args: context.args,
store: context.store
})
const startTime = Date.now()
// 订阅成功回调
context.after((result) => {
console.log('Action finished:', {
name: context.name,
duration: Date.now() - startTime,
result
})
})
// 订阅错误回调
context.onError((error) => {
console.error('Action failed:', {
name: context.name,
duration: Date.now() - startTime,
error
})
})
})
// 取消订阅
// unsubscribe()类型安全:actions 的参数与返回类型如何推导?
Pinia 通过 TypeScript 的类型推导能力,能够自动推导 actions 的参数和返回类型:
export const useProductStore = defineStore('product', () => {
const products = ref<Product[]>([])
// TypeScript 自动推导参数类型和返回类型
async function fetchProducts(filters: {
category?: string;
limit?: number
} = {}) {
const response = await api.getProducts(filters)
this.products = response.data
return response.data // 返回类型自动推导为 Product[]
}
function addProduct(product: Omit<Product, 'id'>): Product {
const newProduct = {
...product,
id: Date.now()
}
this.products.push(newProduct)
return newProduct // 返回类型为 Product
}
function removeProduct(id: number): boolean {
const index = this.products.findIndex(p => p.id === id)
if (index > -1) {
this.products.splice(index, 1)
return true
}
return false
}
return { products, fetchProducts, addProduct, removeProduct }
})
// 在组件中使用时,类型检查会自动生效
const productStore = useProductStore()
// TypeScript 会检查参数类型
productStore.fetchProducts({ category: 'electronics' }) // 正确
productStore.fetchProducts({ category: 123 }) // 类型错误
// 返回值类型也会被正确推导
const newProduct = productStore.addProduct({
name: 'New Product',
price: 99.99
}) // newProduct 类型为 Product小结
在本章中,我们深入探讨了 Pinia 中 actions 的实现机制:
actions是在setup()函数中定义的普通函数- 通过
bind(store)实现this绑定,使this始终指向 store 实例 - 避免使用箭头函数以免丢失
this上下文 - 原生支持异步操作,可直接使用
async/await - 通过
$onAction钩子实现 action 执行的拦截和监控 - TypeScript 自动推导 actions 的参数和返回类型
在下一章中,我们将探讨 getters 的实现机制,包括其作为 computed 封装的本质、缓存机制等内容。
思考题:
- 你在项目中是否使用过
$onAction钩子?用它来解决什么问题? - 对于异步 actions 的错误处理,你通常采用什么策略?