Vue.js 计算属性和侦听器
计算属性 (Computed Properties)
计算属性是基于响应式数据进行计算的属性,它会缓存计算结果,只有当依赖的数据发生变化时才会重新计算。
基本用法
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 计算属性
const fullName = computed(() => {
return firstName.value + lastName.value
})
</script>
<template>
<div>
<p>姓:<input v-model="firstName" /></p>
<p>名:<input v-model="lastName" /></p>
<p>全名:{{ fullName }}</p>
</div>
</template>可写的计算属性
计算属性默认是只读的,但也可以提供 getter 和 setter:
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed({
// getter
get() {
return firstName.value + lastName.value
},
// setter
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
function updateFullName() {
fullName.value = '李 四' // 会触发 setter
}
</script>
<template>
<div>
<p>全名:{{ fullName }}</p>
<button @click="updateFullName">更改为李四</button>
</div>
</template>计算属性 vs 方法
虽然方法也能实现相同的功能,但计算属性有缓存:
vue
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
// 计算属性 - 有缓存
const doubleCount = computed(() => {
console.log('计算属性执行')
return count.value * 2
})
// 方法 - 无缓存
function getDoubleCount() {
console.log('方法执行')
return count.value * 2
}
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<p>双倍(计算属性):{{ doubleCount }}</p>
<p>双倍(计算属性):{{ doubleCount }}</p> <!-- 不会重新计算 -->
<p>双倍(方法):{{ getDoubleCount() }}</p>
<p>双倍(方法):{{ getDoubleCount() }}</p> <!-- 会重新执行 -->
<button @click="count++">增加</button>
</div>
</template>侦听器 (Watchers)
侦听器用于监听响应式数据的变化,并执行相应的操作。
watch 基本用法
vue
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 侦听单个 ref
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})
function increment() {
count.value++
}
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>侦听多个数据源
vue
<script setup>
import { ref, watch } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 侦听多个数据源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名从 ${oldFirst}${oldLast} 变为 ${newFirst}${newLast}`)
})
</script>
<template>
<div>
<input v-model="firstName" placeholder="姓" />
<input v-model="lastName" placeholder="名" />
</div>
</template>侦听响应式对象
vue
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: '张三',
age: 25,
address: {
city: '北京'
}
})
// 侦听整个对象(深度侦听)
watch(user, (newValue, oldValue) => {
console.log('用户信息变化了', newValue)
}, { deep: true })
// 侦听对象的某个属性
watch(() => user.name, (newName, oldName) => {
console.log(`姓名从 ${oldName} 变为 ${newName}`)
})
</script>
<template>
<div>
<input v-model="user.name" placeholder="姓名" />
<input v-model.number="user.age" placeholder="年龄" />
<input v-model="user.address.city" placeholder="城市" />
</div>
</template>watchEffect
watchEffect 会自动追踪其内部使用的响应式数据:
vue
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
// 自动追踪依赖
watchEffect(() => {
console.log(`count: ${count.value}, message: ${message.value}`)
})
// 只要 count 或 message 变化,就会重新执行
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<p>消息:{{ message }}</p>
<button @click="count++">增加计数</button>
<button @click="message = 'Hi'">改变消息</button>
</div>
</template>侦听器选项
vue
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log('count 变化了')
}, {
immediate: true, // 立即执行一次
deep: true, // 深度侦听
flush: 'post' // 在 DOM 更新后执行
})
</script>实战示例:搜索功能
vue
<script setup>
import { ref, watch } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)
// 模拟 API 调用
function searchAPI(query) {
return new Promise(resolve => {
setTimeout(() => {
const results = [
'苹果', '香蕉', '橙子', '葡萄', '西瓜'
].filter(item => item.includes(query))
resolve(results)
}, 500)
})
}
// 侦听搜索关键词
watch(searchQuery, async (newQuery) => {
if (!newQuery) {
searchResults.value = []
return
}
isLoading.value = true
try {
searchResults.value = await searchAPI(newQuery)
} finally {
isLoading.value = false
}
})
</script>
<template>
<div class="search-container">
<input
v-model="searchQuery"
placeholder="搜索水果..."
class="search-input"
/>
<div v-if="isLoading" class="loading">搜索中...</div>
<ul v-else-if="searchResults.length" class="results">
<li v-for="result in searchResults" :key="result">
{{ result }}
</li>
</ul>
<div v-else-if="searchQuery" class="no-results">
没有找到结果
</div>
</div>
</template>
<style scoped>
.search-container {
max-width: 400px;
margin: 20px auto;
}
.search-input {
width: 100%;
padding: 10px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 4px;
}
.loading, .no-results {
padding: 20px;
text-align: center;
color: #666;
}
.results {
list-style: none;
padding: 0;
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.results li {
padding: 10px;
border-bottom: 1px solid #eee;
}
.results li:last-child {
border-bottom: none;
}
.results li:hover {
background-color: #f5f5f5;
}
</style>实战示例:表单验证
vue
<script setup>
import { ref, watch, computed } from 'vue'
const email = ref('')
const password = ref('')
const confirmPassword = ref('')
const emailError = ref('')
const passwordError = ref('')
const confirmPasswordError = ref('')
// 验证邮箱
watch(email, (newEmail) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!newEmail) {
emailError.value = '邮箱不能为空'
} else if (!emailRegex.test(newEmail)) {
emailError.value = '邮箱格式不正确'
} else {
emailError.value = ''
}
})
// 验证密码
watch(password, (newPassword) => {
if (!newPassword) {
passwordError.value = '密码不能为空'
} else if (newPassword.length < 6) {
passwordError.value = '密码至少6个字符'
} else {
passwordError.value = ''
}
})
// 验证确认密码
watch([password, confirmPassword], ([newPassword, newConfirm]) => {
if (!newConfirm) {
confirmPasswordError.value = '请确认密码'
} else if (newPassword !== newConfirm) {
confirmPasswordError.value = '两次密码不一致'
} else {
confirmPasswordError.value = ''
}
})
// 表单是否有效
const isFormValid = computed(() => {
return !emailError.value &&
!passwordError.value &&
!confirmPasswordError.value &&
email.value &&
password.value &&
confirmPassword.value
})
function handleSubmit() {
if (isFormValid.value) {
alert('注册成功!')
}
}
</script>
<template>
<form @submit.prevent="handleSubmit" class="form">
<div class="form-group">
<label>邮箱:</label>
<input v-model="email" type="email" />
<span v-if="emailError" class="error">{{ emailError }}</span>
</div>
<div class="form-group">
<label>密码:</label>
<input v-model="password" type="password" />
<span v-if="passwordError" class="error">{{ passwordError }}</span>
</div>
<div class="form-group">
<label>确认密码:</label>
<input v-model="confirmPassword" type="password" />
<span v-if="confirmPasswordError" class="error">{{ confirmPasswordError }}</span>
</div>
<button type="submit" :disabled="!isFormValid">注册</button>
</form>
</template>
<style scoped>
.form {
max-width: 400px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.error {
display: block;
color: red;
font-size: 14px;
margin-top: 5px;
}
button {
width: 100%;
padding: 10px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>总结
- 计算属性:用于派生数据,有缓存,依赖变化时自动更新
- 侦听器:用于执行副作用,如 API 调用、数据验证等
watch需要明确指定侦听的数据源watchEffect自动追踪依赖- 计算属性适合简单的数据转换,侦听器适合复杂的异步操作
下一步
接下来学习 条件渲染,了解如何根据条件显示或隐藏元素。