计算属性computed深度解析

在Vue 3中,计算属性是一个非常强大的功能,它允许我们基于响应式数据创建派生状态。计算属性具有缓存机制,只有当依赖的响应式数据变化时才会重新计算。本集我们将深入探讨计算属性的实现原理、使用方法和最佳实践。

一、计算属性的基本概念

1. 什么是计算属性?

计算属性是基于响应式依赖进行缓存的属性。当依赖的响应式数据发生变化时,计算属性会自动重新计算;如果依赖没有变化,计算属性会直接返回缓存的结果。

2. 计算属性的优势

  • 缓存机制:避免重复计算,提高性能
  • 声明式语法:更清晰地表达数据之间的依赖关系
  • 响应式更新:自动追踪依赖,当依赖变化时更新
  • 更好的可读性:将复杂的计算逻辑从模板中分离出来

二、计算属性的基本使用

1. 在组合式API中使用

import { ref, computed } from 'vue'

const count = ref(0)

// 只读计算属性
const doubleCount = computed(() => {
  return count.value * 2
})

// 可写计算属性
const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    const [first, last] = newValue.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

2. 在模板中使用

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <p>Full Name: {{ fullName }}</p>
    <input v-model="fullName" placeholder="Enter full name" />
  </div>
</template>

三、计算属性的实现原理

1. 缓存机制

计算属性的核心特性是缓存。Vue会跟踪计算属性依赖的响应式数据,当这些依赖变化时,才会重新计算。

// 计算属性只会在count变化时重新计算
doubleCount.value // 第一次计算,缓存结果
count.value++ // 依赖变化
count.value++ // 依赖变化
count.value++ // 依赖变化
doubleCount.value // 重新计算,缓存新结果
doubleCount.value // 直接返回缓存结果,不重新计算

2. 计算属性的工作流程

  1. 初始化:创建计算属性时,Vue会记录其getter函数
  2. 首次访问:当首次访问计算属性时,执行getter函数并缓存结果
  3. 依赖收集:在执行getter函数过程中,Vue会自动收集所有访问的响应式数据作为依赖
  4. 依赖变化:当任何依赖发生变化时,计算属性会被标记为"脏"(需要重新计算)
  5. 再次访问:当再次访问计算属性时,如果它是"脏"的,就会重新执行getter函数并更新缓存,否则直接返回缓存结果

3. 简化的实现原理

function computed(getterOrOptions) {
  let getter
  let setter

  // 处理传入的参数
  if (typeof getterOrOptions === 'function') {
    getter = getterOrOptions
    setter = () => {
      console.warn('Write operation failed: computed value is readonly')
    }
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true // 标记是否需要重新计算
  let value // 缓存的计算结果
  let scheduler = null // 调度函数

  // 创建副作用函数
  const effect = () => {
    value = getter()
    dirty = false
  }

  // 依赖收集和触发更新的逻辑
  const computedRef = {
    get value() {
      if (dirty) {
        // 执行getter,收集依赖
        effect()
      }
      // 依赖收集:将当前计算属性添加到依赖的响应式数据的依赖列表中
      track(computedRef, 'value')
      return value
    },
    set value(newValue) {
      setter(newValue)
    }
  }

  return computedRef
}

四、计算属性与methods的区别

特性 计算属性 Methods
缓存机制 有,依赖不变时返回缓存结果 无,每次调用都重新执行
调用方式 在模板中直接使用,不需要括号 需要括号调用
响应式更新 自动追踪依赖,依赖变化时更新 每次渲染都重新执行
适用场景 基于现有数据的派生值 事件处理或复杂的命令式逻辑

1. 性能对比

// 计算属性 - 只计算一次,除非deps变化
const expensiveComputed = computed(() => {
  console.log('Computing expensive value...')
  // 模拟昂贵的计算
  for (let i = 0; i < 1000000; i++) {}
  return count.value * 2
})

// Methods - 每次调用都计算
const expensiveMethod = () => {
  console.log('Computing expensive value...')
  // 模拟昂贵的计算
  for (let i = 0; i < 1000000; i++) {}
  return count.value * 2
}

2. 何时使用计算属性 vs Methods

  • 计算属性:当你需要基于响应式数据派生一个值,并且这个值可能会被多次访问时
  • Methods:当你需要执行命令式逻辑,或者每次调用都需要重新计算时

五、计算属性的高级用法

1. 可写计算属性

计算属性默认是只读的,但可以通过提供setter函数来创建可写的计算属性:

import { ref, computed } from 'vue'

const firstName = ref('Alice')
const lastName = ref('Smith')

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    const [first, last] = newValue.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

// 设置计算属性会触发setter
fullName.value = 'Bob Johnson' // 会将firstName设为'Bob',lastName设为'Johnson'

2. 计算属性的依赖跟踪

计算属性会自动跟踪其getter函数中访问的所有响应式数据:

import { ref, computed } from 'vue'

const a = ref(1)
const b = ref(2)
const c = ref(3)

const sum = computed(() => {
  console.log('Computing sum...')
  return a.value + b.value + c.value
})

// 当a、b或c变化时,sum会重新计算

3. 计算属性与watch的结合使用

在某些复杂场景下,可以结合使用计算属性和watch:

import { ref, computed, watch } from 'vue'

const radius = ref(5)

// 计算属性:计算圆的面积
const area = computed(() => {
  return Math.PI * radius.value * radius.value
})

// watch:监听面积变化,执行副作用
watch(area, (newArea, oldArea) => {
  console.log(`Area changed from ${oldArea} to ${newArea}`)
  // 可以执行一些副作用,如发送请求、更新DOM等
})

4. 计算属性的嵌套使用

计算属性可以依赖其他计算属性:

import { ref, computed } from 'vue'

const count = ref(0)

const doubleCount = computed(() => count.value * 2)
const tripleCount = computed(() => doubleCount.value * 1.5) // 依赖doubleCount
const quadrupleCount = computed(() => tripleCount.value * (4/3)) // 依赖tripleCount

5. 计算属性与toRefs结合

import { reactive, computed, toRefs } from 'vue'

const user = reactive({
  firstName: 'Alice',
  lastName: 'Smith',
  age: 30
})

const fullName = computed(() => `${user.firstName} ${user.lastName}`)
const isAdult = computed(() => user.age >= 18)

// 结合toRefs返回
const userRefs = {
  ...toRefs(user),
  fullName,
  isAdult
}

六、计算属性的最佳实践

1. 保持计算属性简洁

计算属性应该只做一件事,并且保持简洁。如果计算逻辑复杂,应该考虑将其拆分为多个计算属性,或者提取为单独的函数:

// 不推荐:复杂的计算逻辑
const complexResult = computed(() => {
  let result = 0
  for (let i = 0; i < items.value.length; i++) {
    if (items.value[i].active) {
      result += items.value[i].price * items.value[i].quantity
    }
  }
  return result * (1 - discount.value / 100)
})

// 推荐:拆分为多个计算属性
const subtotal = computed(() => {
  return items.value
    .filter(item => item.active)
    .reduce((sum, item) => sum + item.price * item.quantity, 0)
})

const total = computed(() => {
  return subtotal.value * (1 - discount.value / 100)
})

2. 避免在计算属性中产生副作用

计算属性应该是纯函数,不应该产生副作用(如修改DOM、发送请求、修改其他响应式数据等):

// 不推荐:在计算属性中产生副作用
const fullName = computed(() => {
  const name = `${firstName.value} ${lastName.value}`
  // 副作用:修改其他响应式数据
  document.title = name
  // 副作用:发送请求
  fetch(`/api/user/${name}`)
  return name
})

// 推荐:使用watch处理副作用
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

watch(fullName, (newName) => {
  document.title = newName
  fetch(`/api/user/${newName}`)
})

3. 注意计算属性的依赖跟踪

计算属性只会跟踪其getter函数中显式访问的响应式数据:

import { ref, computed } from 'vue'

const items = ref([1, 2, 3])
const index = ref(0)

// 计算属性只会跟踪index的变化,不会跟踪items的变化
const currentItem = computed(() => {
  return items.value[index.value]
})

// 修改index会触发currentItem重新计算
index.value++ // currentItem重新计算

// 修改items不会触发currentItem重新计算,除非index指向的元素变化
items.value.push(4) // currentItem不会重新计算
items.value[1] = 100 // currentItem不会重新计算,除非index是1

4. 合理使用可写计算属性

可写计算属性应该用于双向绑定的场景,如表单处理:

import { ref, computed } from 'vue'

const user = reactive({
  firstName: 'Alice',
  lastName: 'Smith'
})

// 可写计算属性,用于双向绑定
const fullName = computed({
  get() {
    return `${user.firstName} ${user.lastName}`
  },
  set(newValue) {
    const [first, last] = newValue.split(' ')
    user.firstName = first || ''
    user.lastName = last || ''
  }
})

七、计算属性的性能优化

1. 避免不必要的计算

计算属性的缓存机制已经提供了很好的性能优化,但在某些情况下,我们还可以进一步优化:

  • 避免在计算属性中执行昂贵的操作:如复杂的循环、DOM操作、网络请求等
  • 拆分复杂的计算属性:将一个复杂的计算属性拆分为多个简单的计算属性
  • 使用v-once或v-memo:在模板中,对于不经常变化的计算属性,可以使用v-once或v-memo进行缓存

2. 注意大型数组的计算

当计算属性依赖大型数组时,要注意性能问题:

// 不推荐:对大型数组进行频繁的计算
const filteredItems = computed(() => {
  return largeArray.value.filter(item => {
    // 复杂的过滤逻辑
    return item.name.includes(searchQuery.value) && 
           item.category === selectedCategory.value &&
           item.price >= minPrice.value &&
           item.price <= maxPrice.value
  })
})

// 推荐:使用防抖或节流
import { ref, computed, watch } from 'vue'

const searchQuery = ref('')
const selectedCategory = ref('')
const minPrice = ref(0)
const maxPrice = ref(1000)

// 防抖后的搜索查询
const debouncedSearchQuery = ref('')

// 使用watch监听搜索条件变化,防抖更新
watch([searchQuery, selectedCategory, minPrice, maxPrice], 
  debounce(([newQuery, newCategory, newMin, newMax]) => {
    debouncedSearchQuery.value = newQuery
    // 更新其他过滤条件...
  }, 300)
)

// 基于防抖后的搜索条件进行计算
const filteredItems = computed(() => {
  return largeArray.value.filter(item => {
    return item.name.includes(debouncedSearchQuery.value) && 
           item.category === selectedCategory.value &&
           item.price >= minPrice.value &&
           item.price <= maxPrice.value
  })
})

八、计算属性的常见错误

1. 忘记使用.value

在组合式API中,计算属性返回的是一个ref对象,需要使用.value访问:

// 错误示例
const doubleCount = computed(() => count.value * 2)
console.log(doubleCount) // 输出:ComputedRefImpl { ... }

// 正确示例
const doubleCount = computed(() => count.value * 2)
console.log(doubleCount.value) // 输出:0

2. 在计算属性中修改依赖

在计算属性的getter函数中修改依赖会导致无限循环:

// 错误示例:无限循环
const count = ref(0)
const incrementedCount = computed(() => {
  count.value++ // 在getter中修改依赖,会导致无限循环
  return count.value
})

// 正确示例:使用watch或methods
const count = ref(0)
const incrementedCount = computed(() => count.value)

watch(count, (newCount) => {
  // 在watch中处理副作用
  console.log(`Count changed to ${newCount}`)
})

3. 过度使用计算属性

计算属性适用于基于响应式数据的派生值,但不适用于所有场景:

// 不推荐:简单的计算,直接在模板中使用表达式
const doubleCount = computed(() => count.value * 2)

// 推荐:对于简单的计算,可以直接在模板中使用
// {{ count * 2 }}

4. 混淆计算属性和watch

计算属性用于派生值,watch用于处理副作用:

// 不推荐:使用watch派生值
const doubleCount = ref(0)
watch(count, (newCount) => {
  doubleCount.value = newCount * 2
})

// 推荐:使用计算属性派生值
const doubleCount = computed(() => count.value * 2)

九、计算属性与相关API的比较

1. 计算属性 vs watch

特性 计算属性 Watch
用途 派生值 处理副作用
缓存
语法 声明式 命令式
自动执行 首次访问时执行 依赖变化时执行
适用场景 基于现有数据计算新值 监听数据变化执行副作用

2. 计算属性 vs methods

特性 计算属性 Methods
缓存
调用方式 直接访问 函数调用
响应式 自动追踪依赖 每次调用重新执行
适用场景 派生值 命令式逻辑

3. 计算属性 vs ref

特性 计算属性 Ref
初始化 基于getter函数 基于初始值
缓存
更新机制 依赖变化时自动更新 手动更新.value
适用场景 派生值 响应式基本类型或对象

十、计算属性的高级特性

1. 计算属性的惰性计算

计算属性是惰性的,只有在被访问时才会首次计算:

import { ref, computed } from 'vue'

const count = ref(0)

const expensiveComputation = computed(() => {
  console.log('Performing expensive computation...')
  // 模拟昂贵的计算
  for (let i = 0; i < 1000000; i++) {}
  return count.value * 2
})

// 此时expensiveComputation还没有执行
console.log('Before accessing expensiveComputation')

// 首次访问,执行计算
console.log(expensiveComputation.value)

// 再次访问,直接返回缓存结果
console.log(expensiveComputation.value)

2. 计算属性的调试

可以使用debuggerconsole.log在计算属性的getter函数中进行调试:

const fullName = computed(() => {
  debugger // 断点调试
  console.log(`Computing fullName from ${firstName.value} and ${lastName.value}`)
  return `${firstName.value} ${lastName.value}`
})

3. 计算属性的类型推断

在TypeScript中,计算属性会自动推断返回类型:

import { ref, computed } from 'vue'

const count = ref(0)

// doubleCount的类型会被推断为number
const doubleCount = computed(() => count.value * 2)

// 也可以显式指定类型
const tripleCount = computed<number>(() => count.value * 3)

十一、总结

1. 核心概念

  • 计算属性是基于响应式依赖进行缓存的属性
  • 计算属性具有缓存机制,只有当依赖变化时才会重新计算
  • 计算属性可以是只读的,也可以是可写的
  • 计算属性应该是纯函数,避免产生副作用

2. 使用原则

  • 当需要基于响应式数据派生新值时,使用计算属性
  • 当计算逻辑复杂或需要多次使用结果时,使用计算属性
  • 当需要执行副作用时,使用watch或methods
  • 保持计算属性简洁,避免复杂逻辑

3. 最佳实践

  • 保持计算属性简洁,只做一件事
  • 避免在计算属性中产生副作用
  • 合理使用缓存机制,避免重复计算
  • 结合使用计算属性和watch处理复杂场景
  • 注意计算属性的依赖跟踪,确保依赖变化时能正确更新

4. 性能优化

  • 避免在计算属性中执行昂贵的操作
  • 对于大型数组的计算,使用防抖或节流
  • 合理拆分复杂的计算属性
  • 避免过度使用计算属性,简单计算可以直接在模板中使用

十二、练习题

  1. 基础练习

    • 创建一个计数器,使用计算属性计算其平方、立方和平方根
    • 在模板中展示这些计算结果
  2. 进阶练习

    • 实现一个购物车功能,使用计算属性计算商品总价、折扣和最终价格
    • 使用可写计算属性实现商品数量的双向绑定
    • 结合watch实现购物车总价变化时的提示
  3. 性能优化练习

    • 实现一个搜索功能,使用计算属性过滤大型数组
    • 添加防抖机制,优化搜索性能
    • 对比使用计算属性和直接在模板中过滤的性能差异
  4. 综合练习

    • 实现一个用户管理系统,使用计算属性处理用户数据
    • 实现用户筛选、排序和分页功能
    • 使用计算属性统计用户数量、平均年龄等信息

十三、扩展阅读

在下一集中,我们将学习"侦听器watch与watchEffect",深入探讨Vue 3中的数据监听机制。

« 上一篇 响应式数据解构与toRefs 下一篇 » 侦听器watch与watchEffect