Skip to content

第三章:掌握 actions 与方法系统

在前两章中,我们深入探讨了 Pinia 的响应式状态实现机制。现在,让我们聚焦于 Pinia 中的 actions 系统,理解其 this 绑定机制、异步处理以及 $onAction 钩子的实现原理。

actions 的本质:定义在 setup() 内部的函数

在 Pinia 中,actions 就是在 setup() 函数内部定义的普通函数。这些函数通过代理机制挂载到 store 实例上,使得它们可以访问 store 的状态和其他方法:

typescript
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 不需要通过 commitdispatch 调用,而是直接作为 store 的方法调用:

typescript
const counter = useCounterStore()
counter.increment() // 直接调用
counter.incrementBy({ amount: 5 }) // 带参数调用
counter.incrementAsync() // 异步调用

this 绑定机制:在 defineStore 内部,actions 函数被 bind(store)

Pinia 中 actions 的一个重要特性是 this 关键字始终指向 store 实例。这是通过在 defineStore 内部将 actions 函数绑定到 store 实例实现的:

typescript
// 简化的 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 的任何属性:

typescript
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 属性。

typescript
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 上下文:

typescript
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 }
})

异步 actionsasync/await 自然支持,返回 Promise

Pinia 对异步操作提供了原生支持,actions 可以直接使用 async/await 语法:

typescript
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 执行前后进行拦截和处理,这对于日志记录、性能监控、错误处理等场景非常有用:

typescript
// 简化的 $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 进行日志记录和监控:

typescript
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 的参数和返回类型:

typescript
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 的实现机制:

  1. actions 是在 setup() 函数中定义的普通函数
  2. 通过 bind(store) 实现 this 绑定,使 this 始终指向 store 实例
  3. 避免使用箭头函数以免丢失 this 上下文
  4. 原生支持异步操作,可直接使用 async/await
  5. 通过 $onAction 钩子实现 action 执行的拦截和监控
  6. TypeScript 自动推导 actions 的参数和返回类型

在下一章中,我们将探讨 getters 的实现机制,包括其作为 computed 封装的本质、缓存机制等内容。


思考题

  1. 你在项目中是否使用过 $onAction 钩子?用它来解决什么问题?
  2. 对于异步 actions 的错误处理,你通常采用什么策略?