Back

Vue.js 中计算属性和侦听器的工作原理

Vue.js 中计算属性和侦听器的工作原理

Vue 的响应式系统会自动跟踪依赖关系,并在数据变化时更新 UI。但是,了解何时使用 Vue 3 计算属性Vue 3 侦听器可能是编写简洁、高性能代码与陷入维护噩梦之间的关键区别。让我们探讨这些特性的工作原理以及各自的适用场景。

核心要点

  • 计算属性是具有缓存机制的声明式函数,用于从响应式数据派生新值
  • 侦听器处理副作用(如 API 调用),非常适合异步操作
  • 对于纯粹的数据转换使用计算属性,对于带有副作用的命令式逻辑使用侦听器
  • 计算属性仅在依赖项发生变化时重新计算,从而提升性能

理解 Vue 的响应式基础

Vue 3 的响应式系统使用 Proxy 来跟踪属性的访问和修改。当响应式数据发生变化时,Vue 能够精确地知道应用程序的哪些部分依赖于该数据,并高效地更新它们。计算属性和侦听器是构建在这一基础之上的两个强大工具,各自在响应式工作流中发挥着不同的作用。

Vue 3 计算属性:声明式且带缓存

计算属性从现有数据派生新的响应式值。它们是声明式的、带缓存的,并且应该始终是没有副作用的纯函数。

组合式 API 语法

<script setup>
import { ref, computed } from 'vue'

const price = ref(100)
const quantity = ref(2)

const total = computed(() => price.value * quantity.value)
const formattedTotal = computed(() => `$${total.value.toFixed(2)}`)
</script>

<template>
  <p>Total: {{ formattedTotal }}</p>
</template>

选项式 API 语法

export default {
  data() {
    return {
      price: 100,
      quantity: 2
    }
  },
  computed: {
    total() {
      return this.price * this.quantity
    },
    formattedTotal() {
      return `$${this.total.toFixed(2)}`
    }
  }
}

关键优势是什么?缓存。Vue 仅在计算属性的依赖项发生变化时才重新计算。如果你在模板中多次引用 formattedTotal,计算只会在每个更新周期运行一次,而不是每次访问都计算。

可写的计算属性

有时,你需要一个同时具有 getter 和 setter 的计算属性:

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

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

Vue 3 侦听器:命令式副作用

侦听器观察响应式值并在它们发生变化时执行副作用。与计算属性不同,侦听器是命令式的,非常适合异步操作、API 调用或 DOM 操作。

基本 Watch 语法

// 组合式 API
import { ref, watch } from 'vue'

const searchQuery = ref('')
const results = ref([])

watch(searchQuery, async (newQuery) => {
  if (newQuery.length > 2) {
    results.value = await fetchSearchResults(newQuery)
  }
})

watchEffect 实现自动跟踪

watchEffect 自动跟踪依赖项,无需显式声明:

import { ref, watchEffect } from 'vue'

const searchQuery = ref('')
const results = ref([])

watchEffect(async () => {
  // 自动跟踪 searchQuery.value
  if (searchQuery.value.length > 2) {
    results.value = await fetchSearchResults(searchQuery.value)
  }
})

高级侦听器选项

watch(source, callback, {
  immediate: true,  // 创建时立即运行
  deep: true,       // 侦听嵌套属性
  flush: 'post'     // DOM 更新后运行
})

计算属性 vs 侦听器:做出正确选择

以下是使用每种方法的时机:

使用计算属性的场景:

  • 从现有响应式数据派生新值
  • 需要在模板中使用结果
  • 操作是同步且纯粹的
  • 需要自动缓存

使用侦听器的场景:

  • 执行副作用(API 调用、日志记录、localStorage)
  • 使用命令式逻辑响应数据变化
  • 处理异步操作
  • 需要访问旧值和新值

实际对比

// ❌ 错误:在计算属性中使用副作用
const searchResults = computed(async () => {
  const response = await fetch(`/api/search?q=${query.value}`)
  return response.json() // 不会工作 - 计算属性必须是同步的
})

// ✅ 正确:使用侦听器处理副作用
watch(query, async (newQuery) => {
  const response = await fetch(`/api/search?q=${newQuery}`)
  searchResults.value = await response.json()
})

// ✅ 正确:使用计算属性进行纯粹的派生
const filteredResults = computed(() => 
  searchResults.value.filter(item => item.active)
)

性能考虑

计算属性通过缓存在性能方面表现出色。它们仅在依赖项发生变化时重新计算,这使得它们非常适合昂贵的操作:

// 仅在 items 或 filterText 变化时重新计算
const expensiveFilter = computed(() => {
  console.log('Filtering...') // 仅在依赖项变化时运行
  return items.value.filter(item => 
    complexFilterLogic(item, filterText.value)
  )
})

然而,侦听器在触发时总是会执行。谨慎使用它们,特别是在大型对象上使用 deep: true 时。

总结

Vue 3 计算属性侦听器仍然是 Vue 响应式系统的基础。记住这个简单的规则:使用计算属性进行纯粹的、带缓存的派生,使用侦听器处理副作用。计算属性通过自动缓存保持模板的简洁和高性能,而侦听器则处理计算属性无法完成的命令式异步操作。掌握这两者,你就能编写既优雅又高效的 Vue 应用程序。

常见问题

不可以,计算属性必须是同步的并立即返回值。对于异步操作,请使用侦听器或方法。计算属性是为响应式数据的纯粹转换而设计的。

当你想要自动依赖跟踪且不需要旧值时使用 watchEffect。当你需要明确控制要侦听的内容、访问旧值或特定配置选项(如深度侦听)时使用 watch。

可以,计算属性会跟踪数组和对象的变化。但是,对于具有嵌套属性的深度响应性,请确保使用 ref 或 reactive 正确地使源数据具有响应性。Vue 3 在这方面比 Vue 2 处理得更好。

计算属性在每个渲染周期只计算一次,无论它被引用多少次。这种缓存行为使得计算属性对于在多个地方使用的昂贵计算非常高效。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay