计算属性 #
什么是计算属性? #
计算属性(Computed Values)是基于现有状态派生出的新状态。它们不存储在 store 中,而是根据需要动态计算。
为什么需要计算属性? #
text
计算属性的作用
├── 避免数据冗余 ─── 不存储可计算的数据
├── 保持数据一致 ─── 派生数据始终正确
├── 简化组件逻辑 ─── 复杂计算封装在 store
└── 提高可维护性 ─── 计算逻辑集中管理
基本实现 #
使用选择器 #
最简单的方式是在组件中使用选择器:
tsx
interface Todo {
id: string
text: string
completed: boolean
}
interface TodoState {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
}
const useTodoStore = create<TodoState>((set) => ({
todos: [],
filter: 'all',
}))
function TodoList() {
const filteredTodos = useTodoStore((state) => {
switch (state.filter) {
case 'active':
return state.todos.filter((t) => !t.completed)
case 'completed':
return state.todos.filter((t) => t.completed)
default:
return state.todos
}
})
return (
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
在 Store 中定义计算函数 #
tsx
interface TodoState {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
// 计算函数
getFilteredTodos: () => Todo[]
getStats: () => { total: number; active: number; completed: number }
}
const useTodoStore = create<TodoState>((set, get) => ({
todos: [],
filter: 'all',
getFilteredTodos: () => {
const { todos, filter } = get()
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed)
case 'completed':
return todos.filter((t) => t.completed)
default:
return todos
}
},
getStats: () => {
const { todos } = get()
return {
total: todos.length,
active: todos.filter((t) => !t.completed).length,
completed: todos.filter((t) => t.completed).length,
}
},
}))
// 使用
function TodoList() {
const filteredTodos = useTodoStore((state) => state.getFilteredTodos())
return <ul>{/* ... */}</ul>
}
function TodoStats() {
const stats = useTodoStore((state) => state.getStats())
return <div>Total: {stats.total}</div>
}
使用 useMemo 优化 #
组件内记忆化 #
tsx
import { useMemo } from 'react'
function TodoList() {
const todos = useTodoStore((state) => state.todos)
const filter = useTodoStore((state) => state.filter)
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed)
case 'completed':
return todos.filter((t) => t.completed)
default:
return todos
}
}, [todos, filter])
return <ul>{/* ... */}</ul>
}
自定义 Hook 封装 #
tsx
function useFilteredTodos() {
const todos = useTodoStore((state) => state.todos)
const filter = useTodoStore((state) => state.filter)
return useMemo(() => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed)
case 'completed':
return todos.filter((t) => t.completed)
default:
return todos
}
}, [todos, filter])
}
function TodoList() {
const filteredTodos = useFilteredTodos()
return <ul>{/* ... */}</ul>
}
高级计算属性模式 #
响应式计算属性 #
tsx
import { useMemo, useEffect, useState } from 'react'
interface ComputedValue<T> {
value: T
dependencies: any[]
}
function useComputed<T>(
selector: () => T,
deps: any[] = []
): T {
return useMemo(selector, deps)
}
// 使用
function TodoStats() {
const stats = useComputed(() => {
const { todos } = useTodoStore.getState()
return {
total: todos.length,
active: todos.filter((t) => !t.completed).length,
}
}, [useTodoStore.getState().todos])
return <div>{stats.total}</div>
}
缓存计算属性 #
tsx
interface CacheEntry<T> {
value: T
timestamp: number
}
function createCachedComputed<T, D extends any[]>(
compute: (...deps: D) => T,
ttl: number = 5000
) {
const cache = new Map<string, CacheEntry<T>>()
return (...deps: D): T => {
const key = JSON.stringify(deps)
const cached = cache.get(key)
if (cached && Date.now() - cached.timestamp < ttl) {
return cached.value
}
const value = compute(...deps)
cache.set(key, { value, timestamp: Date.now() })
return value
}
}
// 使用
const computeExpensiveValue = createCachedComputed(
(data: any[]) => {
// 复杂计算
return data.reduce((acc, item) => acc + item.value, 0)
},
10000 // 10秒缓存
)
链式计算 #
tsx
interface Product {
id: string
name: string
price: number
category: string
}
interface ProductState {
products: Product[]
category: string | null
minPrice: number
maxPrice: number
searchQuery: string
// 计算属性
getFilteredProducts: () => Product[]
getCategories: () => string[]
getPriceRange: () => { min: number; max: number }
}
const useProductStore = create<ProductState>((set, get) => ({
products: [],
category: null,
minPrice: 0,
maxPrice: Infinity,
searchQuery: '',
getFilteredProducts: () => {
const { products, category, minPrice, maxPrice, searchQuery } = get()
return products.filter((product) => {
// 类别过滤
if (category && product.category !== category) return false
// 价格过滤
if (product.price < minPrice || product.price > maxPrice) return false
// 搜索过滤
if (searchQuery && !product.name.toLowerCase().includes(searchQuery.toLowerCase())) {
return false
}
return true
})
},
getCategories: () => {
const { products } = get()
return [...new Set(products.map((p) => p.category))]
},
getPriceRange: () => {
const { products } = get()
if (products.length === 0) return { min: 0, max: 0 }
const prices = products.map((p) => p.price)
return {
min: Math.min(...prices),
max: Math.max(...prices),
}
},
}))
使用第三方库 #
使用 reselect #
tsx
import { createSelector } from 'reselect'
const selectTodos = (state: TodoState) => state.todos
const selectFilter = (state: TodoState) => state.filter
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed)
case 'completed':
return todos.filter((t) => t.completed)
default:
return todos
}
}
)
const selectStats = createSelector(
[selectTodos],
(todos) => ({
total: todos.length,
active: todos.filter((t) => !t.completed).length,
completed: todos.filter((t) => t.completed).length,
})
)
// 使用
function TodoList() {
const filteredTodos = useTodoStore(selectFilteredTodos)
return <ul>{/* ... */}</ul>
}
function TodoStats() {
const stats = useTodoStore(selectStats)
return <div>{stats.total}</div>
}
使用 memoize-one #
tsx
import memoizeOne from 'memoize-one'
const getFilteredTodos = memoizeOne(
(todos: Todo[], filter: string) => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed)
case 'completed':
return todos.filter((t) => t.completed)
default:
return todos
}
}
)
function TodoList() {
const todos = useTodoStore((state) => state.todos)
const filter = useTodoStore((state) => state.filter)
const filteredTodos = getFilteredTodos(todos, filter)
return <ul>{/* ... */}</ul>
}
实际案例 #
购物车计算属性 #
tsx
interface CartItem {
productId: string
name: string
price: number
quantity: number
discount: number // 0-100
}
interface CartState {
items: CartItem[]
taxRate: number
shippingFree: number
shippingCost: number
// 计算属性
getSubtotal: () => number
getDiscount: () => number
getTax: () => number
getShipping: () => number
getTotal: () => number
getItemCount: () => number
}
const useCartStore = create<CartState>((set, get) => ({
items: [],
taxRate: 0.1, // 10%
shippingFree: 100,
shippingCost: 10,
getSubtotal: () => {
const { items } = get()
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
getDiscount: () => {
const { items } = get()
return items.reduce((sum, item) => {
const discount = item.price * (item.discount / 100) * item.quantity
return sum + discount
}, 0)
},
getTax: () => {
const { taxRate } = get()
const subtotal = get().getSubtotal()
const discount = get().getDiscount()
return (subtotal - discount) * taxRate
},
getShipping: () => {
const { shippingFree, shippingCost } = get()
const subtotal = get().getSubtotal()
return subtotal >= shippingFree ? 0 : shippingCost
},
getTotal: () => {
const subtotal = get().getSubtotal()
const discount = get().getDiscount()
const tax = get().getTax()
const shipping = get().getShipping()
return subtotal - discount + tax + shipping
},
getItemCount: () => {
const { items } = get()
return items.reduce((sum, item) => sum + item.quantity, 0)
},
}))
// 组件使用
function CartSummary() {
const getSubtotal = useCartStore((state) => state.getSubtotal)
const getDiscount = useCartStore((state) => state.getDiscount)
const getTax = useCartStore((state) => state.getTax)
const getShipping = useCartStore((state) => state.getShipping)
const getTotal = useCartStore((state) => state.getTotal)
return (
<div>
<p>Subtotal: ${getSubtotal().toFixed(2)}</p>
<p>Discount: -${getDiscount().toFixed(2)}</p>
<p>Tax: ${getTax().toFixed(2)}</p>
<p>Shipping: ${getShipping().toFixed(2)}</p>
<p>Total: ${getTotal().toFixed(2)}</p>
</div>
)
}
用户权限计算 #
tsx
interface Permission {
resource: string
actions: ('read' | 'write' | 'delete')[]
}
interface User {
id: string
name: string
role: 'admin' | 'manager' | 'user'
permissions: Permission[]
}
interface AuthState {
user: User | null
// 计算属性
isAdmin: () => boolean
can: (resource: string, action: 'read' | 'write' | 'delete') => boolean
getAccessibleResources: () => string[]
}
const useAuthStore = create<AuthState>((set, get) => ({
user: null,
isAdmin: () => {
const { user } = get()
return user?.role === 'admin'
},
can: (resource, action) => {
const { user } = get()
if (!user) return false
if (user.role === 'admin') return true
const permission = user.permissions.find((p) => p.resource === resource)
return permission?.actions.includes(action) ?? false
},
getAccessibleResources: () => {
const { user } = get()
if (!user) return []
if (user.role === 'admin') {
return ['all'] // 管理员可以访问所有资源
}
return user.permissions.map((p) => p.resource)
},
}))
// 组件使用
function DeleteButton({ resourceId }: { resourceId: string }) {
const can = useAuthStore((state) => state.can)
if (!can(resourceId, 'delete')) {
return null
}
return <button>Delete</button>
}
function ResourceList() {
const getAccessibleResources = useAuthStore((state) => state.getAccessibleResources)
const resources = getAccessibleResources()
return (
<ul>
{resources.map((resource) => (
<li key={resource}>{resource}</li>
))}
</ul>
)
}
性能优化 #
避免重复计算 #
tsx
// ❌ 不好:每次渲染都计算
function Component() {
const todos = useTodoStore((state) => state.todos)
const completedCount = todos.filter((t) => t.completed).length
return <div>{completedCount}</div>
}
// ✅ 好:使用 useMemo
function Component() {
const todos = useTodoStore((state) => state.todos)
const completedCount = useMemo(
() => todos.filter((t) => t.completed).length,
[todos]
)
return <div>{completedCount}</div>
}
// ✅ 更好:在 store 中定义计算函数
function Component() {
const getCompletedCount = useTodoStore((state) => state.getCompletedCount)
return <div>{getCompletedCount()}</div>
}
使用浅比较 #
tsx
import { shallow } from 'zustand/shallow'
function Component() {
const { todos, filter } = useTodoStore(
(state) => ({ todos: state.todos, filter: state.filter }),
shallow
)
const filteredTodos = useMemo(() => {
// 计算逻辑
}, [todos, filter])
return <ul>{/* ... */}</ul>
}
最佳实践 #
1. 将复杂计算放在 Store 中 #
tsx
// ✅ 好:复杂计算在 store 中
const useStore = create((set, get) => ({
data: [],
getProcessedData: () => {
// 复杂计算逻辑
},
}))
// ❌ 不好:复杂计算在组件中
function Component() {
const data = useStore((state) => state.data)
const processed = complexCalculation(data)
}
2. 使用记忆化避免重复计算 #
tsx
// ✅ 好:使用 useMemo 或 reselect
const filteredTodos = useMemo(() => filterTodos(todos, filter), [todos, filter])
3. 分离计算逻辑 #
tsx
// utils/calculations.ts
export function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
export function calculateDiscount(items: CartItem[]): number {
return items.reduce((sum, item) => {
return sum + item.price * (item.discount / 100) * item.quantity
}, 0)
}
// store/cartStore.ts
import { calculateTotal, calculateDiscount } from '../utils/calculations'
const useCartStore = create((set, get) => ({
items: [],
getTotal: () => calculateTotal(get().items),
getDiscount: () => calculateDiscount(get().items),
}))
总结 #
计算属性的关键点:
- 使用选择器实现简单的计算属性
- 在 Store 中定义计算函数
- 使用 useMemo 或 reselect 优化性能
- 将复杂计算逻辑封装在独立函数中
- 注意缓存和记忆化
接下来,让我们学习 最佳实践,掌握企业级开发规范。
最后更新:2026-03-28