第四章:穿透 getters 与派生状态
在前几章中,我们深入探讨了 Pinia 的状态管理和 actions 系统。现在,让我们聚焦于 Pinia 中的 getters,理解其作为 computed 封装的本质、this 指向机制、缓存机制以及类型推导。
getters 的本质:computed 的封装
在 Pinia 中,getters 是 computed 属性的封装。它们用于定义基于 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 指向:在 getters 中 this 指向 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 的实现机制:
getters本质上是computed属性的封装- 通过
computed(() => getter.call(store))实现this指向 store 实例 - 继承了 Vue
computed的缓存机制,具有惰性求值和脏检查特性 - 可以通过返回函数的方式实现带参数的 getters(但失去缓存特性)
- TypeScript 能够自动推导 getters 的返回类型
- 与 Vue Composition API 无缝集成
在下一章中,我们将探讨 Pinia 的插件系统,包括 pinia.use() 机制、持久化插件实现等内容。
思考题:
- 你在项目中如何使用 getters 来优化性能?有没有遇到缓存相关的问题?
- 对于带参数的 getters,你通常如何处理其无法缓存的特性?