Vue.js 提供了强大的响应式计算能力,主要通过计算属性(computed)和侦听器(watch)来实现。这两种特性使得我们能够基于响应式数据执行复杂的计算和逻辑,同时保持代码的简洁和高效。
计算属性是基于它们的响应式依赖进行缓存的派生值。只有在相关响应式依赖发生改变时才会重新求值。这使得计算属性非常高效,特别适用于复杂计算。
计算属性具有缓存机制:如果依赖的响应式数据没有变化,计算属性会直接返回缓存的结果,不会重新计算。
// 导入 computed
import { ref, computed } from 'vue'
// 响应式数据
const firstName = ref('张')
const lastName = ref('三')
// 计算属性
const fullName = computed(() => {
console.log('计算属性执行了')
return firstName.value + lastName.value
})
// 使用计算属性
console.log(fullName.value) // 输出: 张三
// 修改依赖数据
firstName.value = '李'
console.log(fullName.value) // 输出: 李三
// 再次访问计算属性(依赖未改变,使用缓存)
console.log(fullName.value) // 不会输出"计算属性执行了"
export default {
data() {
return {
firstName: '张',
lastName: '三'
}
},
computed: {
// 计算属性定义
fullName() {
console.log('计算属性执行了')
return this.firstName + this.lastName
},
// 计算属性也可以有 setter
reversedFullName: {
get() {
return this.lastName + this.firstName
},
set(newValue) {
const [last, first] = newValue.split('')
this.lastName = last
this.firstName = first
}
}
},
methods: {
updateName() {
this.firstName = '李'
console.log(this.fullName) // 输出: 李三
}
}
}
import { ref, computed } from 'vue'
// 响应式数据
const cartItems = ref([
{ id: 1, name: '商品A', price: 100, quantity: 2 },
{ id: 2, name: '商品B', price: 200, quantity: 1 },
{ id: 3, name: '商品C', price: 50, quantity: 3 }
])
// 计算总数量
const totalQuantity = computed(() => {
return cartItems.value.reduce((total, item) => total + item.quantity, 0)
})
// 计算总价
const totalPrice = computed(() => {
return cartItems.value.reduce((total, item) => total + (item.price * item.quantity), 0)
})
// 计算折扣价(假设满500打9折)
const discountedPrice = computed(() => {
const total = totalPrice.value
return total >= 500 ? total * 0.9 : total
})
// 计算平均价格
const averagePrice = computed(() => {
return totalQuantity.value > 0 ? totalPrice.value / totalQuantity.value : 0
})
// 使用计算属性
console.log(`总数量: ${totalQuantity.value}`) // 6
console.log(`总价: ${totalPrice.value}`) // 550
console.log(`折扣价: ${discountedPrice.value}`) // 495
console.log(`平均价格: ${averagePrice.value.toFixed(2)}`) // 91.67
计算属性默认是只读的,但你可以通过提供 get 和 set 函数来创建一个可写的计算属性。
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 可写计算属性
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
const names = newValue.split(' ')
if (names.length >= 2) {
firstName.value = names[0]
lastName.value = names[1]
}
}
})
// 使用 getter
console.log(fullName.value) // 张 三
// 使用 setter
fullName.value = '李 四'
console.log(firstName.value) // 李
console.log(lastName.value) // 四
侦听器用于观察和响应 Vue 实例上的数据变动。当需要在数据变化时执行异步或开销较大的操作时,侦听器是最有用的。
侦听器适用于:执行异步操作、响应数据变化执行复杂逻辑、监听多个数据源、执行副作用操作。
import { ref, watch } from 'vue'
// 响应式数据
const count = ref(0)
const username = ref('')
// 侦听单个数据源
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})
// 侦听多个数据源
watch([count, username], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}, name: ${oldName} -> ${newName}`)
})
// 立即执行侦听器
watch(count, (newValue, oldValue) => {
console.log(`当前 count 值: ${newValue}`)
}, { immediate: true })
// 深度侦听对象
const user = ref({ name: '张三', age: 25 })
watch(user, (newValue, oldValue) => {
console.log('user 对象发生变化:', newValue)
}, { deep: true })
// 触发变化
count.value++ // 输出: count 从 0 变为 1
username.value = '李四' // 输出: count: 1 -> 1, name: -> 李四
user.value.age = 26 // 输出: user 对象发生变化: { name: '张三', age: 26 }
export default {
data() {
return {
count: 0,
username: '',
user: {
name: '张三',
age: 25
}
}
},
watch: {
// 侦听简单属性
count(newValue, oldValue) {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
},
// 侦听对象属性
'user.name'(newValue, oldValue) {
console.log(`用户名从 ${oldValue} 变为 ${newValue}`)
},
// 深度侦听对象
user: {
handler(newValue, oldValue) {
console.log('user 对象发生变化:', newValue)
},
deep: true,
immediate: true // 立即执行一次
}
},
methods: {
updateData() {
this.count++
this.user.name = '李四'
}
}
}
import { ref, watch } from 'vue'
// 搜索关键词
const searchQuery = ref('')
// 搜索结果
const searchResults = ref([])
// 加载状态
const isLoading = ref(false)
// 防抖函数
function debounce(func, delay) {
let timer
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
// 搜索函数
async function performSearch(query) {
if (!query.trim()) {
searchResults.value = []
return
}
isLoading.value = true
try {
// 模拟API调用
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
const data = await response.json()
searchResults.value = data.results
} catch (error) {
console.error('搜索失败:', error)
searchResults.value = []
} finally {
isLoading.value = false
}
}
// 防抖后的搜索函数
const debouncedSearch = debounce(performSearch, 500)
// 侦听搜索关键词变化
watch(searchQuery, (newQuery) => {
debouncedSearch(newQuery)
})
// 当用户输入时,会自动触发搜索
searchQuery.value = 'Vue.js'
| 特性 | 计算属性 (computed) | 方法 (methods) | 侦听器 (watch) |
|---|---|---|---|
| 缓存 | ✅ 有缓存,依赖不变不重新计算 | ❌ 每次调用都重新计算 | ❌ 不缓存计算结果 |
| 异步操作 | ❌ 不支持异步操作 | ✅ 支持异步操作 | ✅ 支持异步操作 |
| 返回值 | ✅ 必须返回一个值 | ✅ 可以返回任何值 | ❌ 没有返回值(执行副作用) |
| 使用场景 | 派生数据、复杂计算 | 事件处理、通用函数 | 数据变化时的响应操作 |
| 语法 | computed(() => {}) |
method() {} |
watch(source, callback) |
| 模板使用 | {{ computedValue }} |
{{ method() }} 或 @click="method" |
不在模板中直接使用 |
import { ref, computed, watch } from 'vue'
// 优化前:每次访问都会重新计算
const items = ref([/* 大量数据 */])
const expensiveCalculation = () => {
console.log('执行昂贵计算')
return items.value
.filter(item => item.active)
.map(item => ({ ...item, processed: true }))
.sort((a, b) => b.priority - a.priority)
}
// 优化后:使用计算属性进行缓存
const optimizedCalculation = computed(() => {
console.log('执行昂贵计算(有缓存)')
return items.value
.filter(item => item.active)
.map(item => ({ ...item, processed: true }))
.sort((a, b) => b.priority - a.priority)
})
// 优化侦听器:避免深度侦听大型对象
const largeObject = ref({ /* 大型对象 */ })
watch(
() => largeObject.value.specificProperty, // 只侦听需要的属性
(newValue) => {
console.log('specificProperty 变化了:', newValue)
}
)
// 使用 watchEffect 自动追踪依赖
import { watchEffect } from 'vue'
watchEffect(() => {
// 自动追踪所有访问过的响应式属性
console.log('items 长度:', items.value.length)
console.log('第一个项目:', items.value[0]?.name)
})
<template>
<div class="todo-app">
<h2>待办事项</h2>
<div class="mb-3">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="输入新待办"
class="form-control"
>
<button @click="addTodo" class="btn btn-primary mt-2">添加</button>
</div>
<div class="filters mb-3">
<button
@click="filter = 'all'"
:class="{ active: filter === 'all' }"
class="btn btn-sm me-2"
>
全部 ({{ totalCount }})
</button>
<button
@click="filter = 'active'"
:class="{ active: filter === 'active' }"
class="btn btn-sm me-2"
>
待办 ({{ activeCount }})
</button>
<button
@click="filter = 'completed'"
:class="{ active: filter === 'completed' }"
class="btn btn-sm"
>
已完成 ({{ completedCount }})
</button>
</div>
<ul class="list-group">
<li
v-for="todo in filteredTodos"
:key="todo.id"
class="list-group-item d-flex justify-content-between align-items-center"
>
<div>
<input
type="checkbox"
v-model="todo.completed"
class="me-2"
>
<span :class="{ 'text-decoration-line-through': todo.completed }">
{{ todo.text }}
</span>
</div>
<button @click="removeTodo(todo.id)" class="btn btn-sm btn-danger">
×
</button>
</li>
</ul>
<div class="stats mt-3">
<p>统计信息: {{ statsMessage }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
// 响应式数据
const todos = ref([
{ id: 1, text: '学习 Vue.js', completed: true },
{ id: 2, text: '编写示例代码', completed: false },
{ id: 3, text: '阅读文档', completed: false }
])
const newTodo = ref('')
const filter = ref('all')
// 计算属性
const totalCount = computed(() => todos.value.length)
const activeCount = computed(() => todos.value.filter(t => !t.completed).length)
const completedCount = computed(() => todos.value.filter(t => t.completed).length)
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.completed)
case 'completed':
return todos.value.filter(t => t.completed)
default:
return todos.value
}
})
const statsMessage = computed(() => {
const active = activeCount.value
const total = totalCount.value
const percentage = total > 0 ? Math.round((active / total) * 100) : 0
return `还有 ${active} 项待办 (${percentage}% 未完成)`
})
// 方法
function addTodo() {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
completed: false
})
newTodo.value = ''
}
}
function removeTodo(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
// 侦听器:保存数据到本地存储
watch(todos, (newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })
// 侦听器:变化提示
watch(activeCount, (newCount, oldCount) => {
if (newCount < oldCount) {
console.log('恭喜!完成了一项任务!')
}
})
// 初始化:从本地存储加载数据
const savedTodos = localStorage.getItem('todos')
if (savedTodos) {
todos.value = JSON.parse(savedTodos)
}
</script>
<style scoped>
.todo-app {
max-width: 500px;
margin: 0 auto;
}
.active {
background-color: #007bff;
color: white;
}
.list-group-item {
transition: background-color 0.3s;
}
.list-group-item:hover {
background-color: #f8f9fa;
}
</style>
watchEffect 自动追踪依赖Vue.js 的响应式计算系统提供了强大的工具来处理派生数据和响应数据变化。计算属性适合处理需要缓存的派生数据,而侦听器适合处理数据变化时的副作用和异步操作。
理解计算属性和侦听器的区别及适用场景,能够帮助你编写更高效、更易维护的 Vue 应用。记住:
在实际开发中,合理使用这些特性可以显著提升应用性能。