跳到主要内容

Vue computed 和 watch

Vue 提供了两个重要的 API 来处理响应式数据的变化:computed 用于创建计算属性,watch 用于监听数据变化并执行相应的操作。

computed()

computed() 用于创建计算属性,它基于响应式数据进行计算,并缓存结果。

基本用法

import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 创建计算属性
const fullName = computed(() => {
return firstName.value + lastName.value
})

console.log(fullName.value) // '张三'

// 修改依赖的数据
firstName.value = '李'
console.log(fullName.value) // '李三'

计算属性 vs 方法

计算属性和方法的主要区别在于缓存

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

const items = ref([1, 2, 3, 4, 5])

// 计算属性 - 有缓存,只有依赖变化时才重新计算
const sum = computed(() => {
console.log('计算中...') // 只执行一次
return items.value.reduce((a, b) => a + b, 0)
})

// 方法 - 每次调用都会执行
function getSum() {
console.log('计算中...') // 每次调用都执行
return items.value.reduce((a, b) => a + b, 0)
}
<template>
<p>计算属性: {{ sum }}</p> <!-- 只计算一次 -->
<p>计算属性: {{ sum }}</p> <!-- 使用缓存 -->
<p>方法: {{ getSum() }}</p> <!-- 每次都重新计算 -->
<p>方法: {{ getSum() }}</p> <!-- 每次都重新计算 -->
</template>

可写的计算属性

计算属性默认是只读的,但可以通过 get 和 set 创建可写的计算属性:

import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

const fullName = computed({
get() {
return firstName.value + lastName.value
},
set(value) {
// 当设置 fullName 时,自动拆分到 firstName 和 lastName
firstName.value = value.slice(0, 1)
lastName.value = value.slice(1)
}
})

console.log(fullName.value) // '张三'

fullName.value = '李四'
console.log(firstName.value) // '李'
console.log(lastName.value) // '四'

实际示例:购物车

import { ref, computed } from 'vue'

const cart = ref([
{ name: '苹果', price: 5, quantity: 2 },
{ name: '香蕉', price: 3, quantity: 1 },
{ name: '橙子', price: 4, quantity: 3 }
])

// 计算总数量
const totalQuantity = computed(() => {
return cart.value.reduce((sum, item) => sum + item.quantity, 0)
})

// 计算总价
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})

// 计算折扣后价格
const discountedPrice = computed({
get() {
return totalPrice.value * 0.9 // 9折
},
set(value) {
// 根据折扣后价格反推原价
const originalPrice = value / 0.9
console.log('原价:', originalPrice.toFixed(2))
}
})

watch()

watch() 用于监听数据变化并执行相应的操作。

基本用法

import { ref, watch } from 'vue'

const count = ref(0)

// 监听 count 的变化
watch(count, (newValue, oldValue) => {
console.log(`count 变化: ${oldValue} -> ${newValue}`)
})

count.value++ // 输出: count 变化: 0 -> 1

监听多个数据源

import { ref, watch } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 监听多个数据源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`${oldFirst} -> ${newFirst}, ${oldLast} -> ${newLast}`)
})

firstName.value = '李' // 输出: 张 -> 李, 三 -> 三

深度监听

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

const state = reactive({
user: {
name: '张三',
age: 25
}
})

// 深度监听
watch(state, (newValue, oldValue) => {
console.log('state 变化:', newValue)
}, { deep: true })

state.user.age = 30 // 触发监听

立即执行

import { ref, watch } from 'vue'

const message = ref('')

// 添加 immediate 选项,立即执行一次
watch(message, (newValue) => {
console.log('消息:', newValue)
}, { immediate: true })

// 输出: 消息: (初始值)

获取新值和旧值

import { reactive, watch } from 'vue'

const state = reactive({ count: 0 })

watch(state, (newValue, oldValue) => {
console.log('新值:', newValue)
console.log('旧值:', oldValue)
// 注意:对于 reactive 对象,新值和旧值是同一个对象
})

state.count = 1
// 新值: { count: 1 }
// 旧值: { count: 1 } // 相同引用!

监听特定属性

import { reactive, watch } from 'vue'

const state = reactive({
user: {
name: '张三',
age: 25
}
})

// 只监听 user.name 的变化
watch(() => state.user.name, (newValue, oldValue) => {
console.log(`name: ${oldValue} -> ${newValue}`)
})

state.user.name = '李四' // 触发
state.user.age = 30 // 不触发

watchEffect()

watchEffect() 会立即执行传入的回调函数,并自动追踪回调中使用的所有响应式数据。

基本用法

import { ref, watchEffect } from 'vue'

const count = ref(0)
const name = ref('张三')

watchEffect(() => {
console.log(`${name.value}: ${count.value}`)
})

count.value++ // 输出: 张三: 1
name.value = '李四' // 输出: 李四: 1

watch vs watchEffect

特性watchwatchEffect
惰性执行否(默认惰性,可配置 immediate)是(立即执行)
获取新值/旧值支持不支持
明确指定依赖需要(第一个参数)自动追踪
适用场景需要获取变化前后的值只需要响应式依赖变化

选择建议

// 使用 watchEffect
// - 不关心旧值
// - 想在初始化时执行
// - 依赖明确,不需要显式指定

watchEffect(() => {
console.log(searchQuery.value)
fetchResults(searchQuery.value)
})

// 使用 watch
// - 需要访问旧值
// - 只在特定条件下监听
// - 明确的依赖关系

watch(searchQuery, (newValue, oldValue) => {
console.log(`${oldValue} -> ${newValue}`)
}, { immediate: true })

停止监听

自动停止

setup()<script setup> 中使用 watch,组件卸载时会自动停止:

<script setup>
import { watch } from 'vue'

const count = ref(0)

watch(count, () => {
console.log(count.value)
})
// 组件卸载时自动停止
</script>

手动停止

使用 watch 返回的停止函数:

import { watch } from 'vue'

const count = ref(0)

const stop = watch(count, () => {
console.log(count.value)
})

// 停止监听
stop()

count.value++ // 不再触发

实际示例:搜索功能

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

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

// 使用 watchEffect 自动搜索
watchEffect(async () => {
if (!searchQuery.value) {
results.value = []
return
}

isLoading.value = true
try {
const response = await fetch(`/api/search?q=${searchQuery.value}`)
results.value = await response.json()
} finally {
isLoading.value = false
}
})

// 使用 watch 监听特定变化
watch(results, (newResults) => {
console.log('搜索结果更新:', newResults.length)
}, { deep: true })
</script>

<template>
<div>
<input v-model="searchQuery" placeholder="搜索..." />
<p v-if="isLoading">加载中...</p>
<ul>
<li v-for="result in results" :key="result.id">
{{ result.name }}
</li>
</ul>
</div>
</template>

示例:表单验证

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

const email = ref('')
const errors = ref({
email: ''
})

const isValidEmail = computed(() => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email.value)
})

watch(email, (newValue) => {
if (!newValue) {
errors.value.email = '邮箱不能为空'
} else if (!isValidEmail.value) {
errors.value.email = '邮箱格式不正确'
} else {
errors.value.email = ''
}
})

const isFormValid = computed(() => {
return !errors.value.email
})
</script>

<template>
<form>
<div>
<input v-model="email" type="email" placeholder="邮箱" />
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<button :disabled="!isFormValid">提交</button>
</div>
</template>

小结

本章我们详细学习了 computed 和 watch 的完整内容:

  1. computed():创建计算属性,自动缓存
  2. 可写计算属性:使用 get 和 set
  3. watch():监听数据变化,获取新旧值
  4. watchEffect():自动追踪依赖的响应式数据
  5. 深度监听:监听对象深层变化
  6. 停止监听:自动和手动停止

练习

  1. 创建一个实时搜索的组件
  2. 实现一个表单验证系统
  3. 创建一个计算器组件

准备好进入下一章,学习组合式 API 的高级用法的内容了吗?