Skip to content

第四章:穿透 getters 与派生状态

在前几章中,我们深入探讨了 Pinia 的状态管理和 actions 系统。现在,让我们聚焦于 Pinia 中的 getters,理解其作为 computed 封装的本质、this 指向机制、缓存机制以及类型推导。

getters 的本质:computed 的封装

在 Pinia 中,getterscomputed 属性的封装。它们用于定义基于 store 状态的派生属性,具有响应式和缓存特性:

typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const user = ref({
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
    permissions: ['read', 'write']
  })
  
  // 这些就是 getters
  const fullName = computed(() => {
    return `${user.value.firstName} ${user.value.lastName}`
  })
  
  const isAdult = computed(() => {
    return user.value.age >= 18
  })
  
  const canWrite = computed(() => {
    return user.value.permissions.includes('write')
  })
  
  return { user, fullName, isAdult, canWrite }
})

与直接使用 computed 不同,Pinia 的 getters 提供了更多的集成特性,如 this 指向、类型推导等。

简化的 getters 实现原理

typescript
// 简化的 getters 实现
function createGetters(store, gettersDefinition) {
  const getters = {}
  
  for (const [name, getterFn] of Object.entries(gettersDefinition)) {
    // 使用 computed 包装 getter 函数
    getters[name] = computed(() => {
      // 绑定 this 指向 store 实例
      return getterFn.call(store)
    })
  }
  
  return getters
}

// 在 defineStore 中的处理
function defineStore(id, setup) {
  return function useStore() {
    const setupResult = setup()
    
    // 分离状态、getters 和 actions
    const state = {}
    const getters = {}
    const actions = {}
    
    for (const [key, value] of Object.entries(setupResult)) {
      if (typeof value === 'function' && isGetter(value)) {
        getters[key] = value
      } else if (typeof value === 'function') {
        actions[key] = value
      } else {
        state[key] = value
      }
    }
    
    // 创建 store 实例
    const store = reactive({
      ...state,
      ...createGetters(store, getters), // 处理 getters
      ...bindActions(store, actions)    // 处理 actions
    })
    
    return store
  }
}

this 指向:在 gettersthis 指向 store,可访问其他 getters

Pinia 中的 getters 可以通过 this 访问 store 的任何属性,包括状态和其他 getters:

typescript
export const useCartStore = defineStore('cart', () => {
  const items = ref([
    { name: 'Apple', price: 1.2, quantity: 2 },
    { name: 'Banana', price: 0.8, quantity: 3 }
  ])
  
  const taxRate = ref(0.08)
  
  // 访问 state
  const subtotal = computed(() => {
    return this.items.reduce((total, item) => {
      return total + (item.price * item.quantity)
    }, 0)
  })
  
  // 访问其他 getters
  const tax = computed(() => {
    return this.subtotal * this.taxRate
  })
  
  // 访问 state 和其他 getters
  const total = computed(() => {
    return this.subtotal + this.tax
  })
  
  // 带参数的 getters(返回函数)
  const getItemTotal = computed(() => {
    return (itemName) => {
      const item = this.items.find(i => i.name === itemName)
      return item ? item.price * item.quantity : 0
    }
  })
  
  return { items, taxRate, subtotal, tax, total, getItemTotal }
})

// 使用示例
const cartStore = useCartStore()
console.log(cartStore.subtotal) // 4.8
console.log(cartStore.tax)      // 0.384
console.log(cartStore.total)    // 5.184
console.log(cartStore.getItemTotal('Apple')) // 2.4

实现方式:computed(() => getter.call(store))

Pinia 通过将 getter 函数绑定到 store 实例来实现 this 指向:

typescript
// 简化的实现方式
function createComputedGetters(store, getters) {
  const computedGetters = {}
  
  for (const [name, getter] of Object.entries(getters)) {
    computedGetters[name] = computed(() => {
      // 通过 call 绑定 this 指向 store
      return getter.call(store)
    })
  }
  
  return computedGetters
}

这种实现方式确保了在 getters 中可以通过 this 访问 store 的所有属性。

缓存机制:computed 的惰性求值与脏检查

Pinia 的 getters 继承了 Vue computed 的缓存机制,只有依赖的状态发生变化时才会重新计算:

typescript
export const useExpensiveStore = defineStore('expensive', () => {
  const items = ref([])
  const filter = ref('')
  
  // 昂贵的计算过程
  const filteredItems = computed(() => {
    console.log('Computing filtered items...') // 用于观察计算时机
    
    // 模拟昂贵的计算
    return this.items
      .filter(item => item.name.includes(this.filter))
      .map(item => {
        // 模拟复杂处理
        return {
          ...item,
          processed: true,
          timestamp: Date.now()
        }
      })
  })
  
  // 依赖 filteredItems 的另一个 getter
  const filteredCount = computed(() => {
    console.log('Computing filtered count...')
    return this.filteredItems.length
  })
  
  function addItem(item) {
    this.items.push(item)
  }
  
  function setFilter(newFilter) {
    this.filter = newFilter
  }
  
  return { items, filter, filteredItems, filteredCount, addItem, setFilter }
})

// 使用示例
const store = useExpensiveStore()

// 第一次访问,会执行计算
console.log(store.filteredItems) // 输出: Computing filtered items...
console.log(store.filteredCount) // 输出: Computing filtered count...

// 再次访问,使用缓存值
console.log(store.filteredItems) // 无输出,使用缓存
console.log(store.filteredCount) // 无输出,使用缓存

// 修改依赖,触发重新计算
store.setFilter('test')
console.log(store.filteredItems) // 输出: Computing filtered items...
console.log(store.filteredCount) // 输出: Computing filtered count...

带参数的 Getters

虽然 getters 本身是缓存的,但我们可以通过返回函数的方式来实现带参数的"getters":

typescript
export const useProductStore = defineStore('product', () => {
  const products = ref([
    { id: 1, name: 'Laptop', category: 'electronics', price: 1000 },
    { id: 2, name: 'Book', category: 'education', price: 20 },
    { id: 3, name: 'Phone', category: 'electronics', price: 500 }
  ])
  
  // 带参数的 getter(返回函数)
  const getProductById = computed(() => {
    return (id) => {
      return this.products.find(product => product.id === id)
    }
  })
  
  const getProductsByCategory = computed(() => {
    return (category) => {
      return this.products.filter(product => product.category === category)
    }
  })
  
  // 注意:这种方式返回的函数不会被缓存
  const getProductByName = computed(() => {
    return (name) => {
      console.log('Searching for product by name...') // 每次都会执行
      return this.products.find(product => product.name === name)
    }
  })
  
  return { products, getProductById, getProductsByCategory, getProductByName }
})

// 使用示例
const productStore = useProductStore()
const laptop = productStore.getProductById(1) // 每次调用都会执行函数
const electronics = productStore.getProductsByCategory('electronics') // 每次调用都会执行函数

类型推导:getters 如何推导返回类型?

Pinia 通过 TypeScript 的类型推导能力,能够自动推导 getters 的返回类型:

typescript
interface User {
  firstName: string
  lastName: string
  age: number
  permissions: string[]
}

export const useUserStore = defineStore('user', () => {
  const user = ref<User>({
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
    permissions: ['read', 'write']
  })
  
  // TypeScript 自动推导返回类型为 string
  const fullName = computed(() => {
    return `${this.user.firstName} ${this.user.lastName}`
  })
  
  // TypeScript 自动推导返回类型为 boolean
  const isAdult = computed(() => {
    return this.user.age >= 18
  })
  
  // TypeScript 自动推导返回类型为 string[]
  const permissionsList = computed(() => {
    return this.user.permissions
  })
  
  // 复杂类型的推导
  const userSummary = computed(() => {
    return {
      name: this.fullName,
      isAdult: this.isAdult,
      permissionCount: this.user.permissions.length
    }
  }) // 推导为 { name: string; isAdult: boolean; permissionCount: number }
  
  return { user, fullName, isAdult, permissionsList, userSummary }
})

// 在组件中使用时,类型检查会自动生效
const userStore = useUserStore()
const name: string = userStore.fullName     // 正确
const adult: boolean = userStore.isAdult    // 正确
// const invalid: number = userStore.fullName // 类型错误

Getters 与 Composition API 的结合

Pinia 的 getters 可以很好地与 Vue 的 Composition API 结合使用:

typescript
// stores/chart.ts
export const useChartStore = defineStore('chart', () => {
  const data = ref<number[]>([])
  
  const processedData = computed(() => {
    return this.data.map((value, index) => ({
      x: index,
      y: value,
      normalized: value / Math.max(...this.data)
    }))
  })
  
  const statistics = computed(() => {
    const values = this.data
    if (values.length === 0) return null
    
    return {
      min: Math.min(...values),
      max: Math.max(...values),
      avg: values.reduce((a, b) => a + b, 0) / values.length,
      median: [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)]
    }
  })
  
  function addData(value: number) {
    this.data.push(value)
  }
  
  return { data, processedData, statistics, addData }
})

// 在组件中使用
import { useChartStore } from '@/stores/chart'

export default {
  setup() {
    const chartStore = useChartStore()
    
    // 可以直接解构 getters,保持响应性
    const { processedData, statistics } = storeToRefs(chartStore)
    
    return {
      processedData,
      statistics,
      chartStore
    }
  }
}

小结

在本章中,我们深入探讨了 Pinia 中 getters 的实现机制:

  1. getters 本质上是 computed 属性的封装
  2. 通过 computed(() => getter.call(store)) 实现 this 指向 store 实例
  3. 继承了 Vue computed 的缓存机制,具有惰性求值和脏检查特性
  4. 可以通过返回函数的方式实现带参数的 getters(但失去缓存特性)
  5. TypeScript 能够自动推导 getters 的返回类型
  6. 与 Vue Composition API 无缝集成

在下一章中,我们将探讨 Pinia 的插件系统,包括 pinia.use() 机制、持久化插件实现等内容。


思考题

  1. 你在项目中如何使用 getters 来优化性能?有没有遇到缓存相关的问题?
  2. 对于带参数的 getters,你通常如何处理其无法缓存的特性?