计算属性 #

什么是计算属性? #

计算属性(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