Appearance
Vue.js 计算属性与侦听器
Vue.js 提供了计算属性和侦听器两种机制来处理数据的派生和副作用。
计算属性 (Computed Properties)
基础用法
计算属性是基于它们的响应式依赖进行缓存的,只有依赖发生改变时才会重新计算:
vue
<template>
<div>
<h3>基础计算属性</h3>
<p>原始消息: "{{ message }}"</p>
<p>反转消息: "{{ reversedMessage }}"</p>
<p>反转消息(方法): "{{ reverseMessage() }}"</p>
<button @click="updateMessage">更新消息</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue!'
}
},
computed: {
// 计算属性的 getter
reversedMessage() {
console.log('计算属性被调用') // 只有在依赖变化时才会调用
return this.message.split('').reverse().join('')
}
},
methods: {
// 方法 - 每次渲染都会调用
reverseMessage() {
console.log('方法被调用') // 每次渲染都会调用
return this.message.split('').reverse().join('')
},
updateMessage() {
this.message = `更新时间: ${new Date().toLocaleTimeString()}`
}
}
}
</script>
计算属性的 setter
计算属性可以设置 getter 和 setter:
vue
<template>
<div>
<h3>带 setter 的计算属性</h3>
<p>姓名: {{ fullName }}</p>
<button @click="updateFullName">更新全名</button>
<button @click="updateFirstName">更新名</button>
<button @click="updateLastName">更新姓</button>
</div>
</template>
<script>
export default {
data() {
return {
firstName: '张',
lastName: '三'
}
},
computed: {
fullName: {
// getter
get() {
return `${this.firstName} ${this.lastName}`
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0] || ''
this.lastName = names[1] || ''
}
}
},
methods: {
updateFullName() {
this.fullName = '李 四'
},
updateFirstName() {
this.firstName = '王'
},
updateLastName() {
this.lastName = '五'
}
}
}
</script>
复杂计算属性
vue
<template>
<div>
<h3>复杂计算属性</h3>
<div>
<input v-model="searchQuery" placeholder="搜索用户">
<label>
<input type="checkbox" v-model="showActiveOnly"> 只显示活跃用户
</label>
</div>
<p>总用户数: {{ totalUsers }}</p>
<p>活跃用户数: {{ activeUsersCount }}</p>
<p>搜索结果数: {{ searchResults.length }}</p>
<ul>
<li v-for="user in searchResults" :key="user.id">
{{ user.name }} - {{ user.status }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: '',
showActiveOnly: false,
users: [
{ id: 1, name: '张三', status: 'active', age: 25 },
{ id: 2, name: '李四', status: 'inactive', age: 30 },
{ id: 3, name: '王五', status: 'active', age: 28 },
{ id: 4, name: '赵六', status: 'active', age: 35 },
{ id: 5, name: '钱七', status: 'inactive', age: 22 }
]
}
},
computed: {
// 总用户数
totalUsers() {
return this.users.length
},
// 活跃用户数
activeUsersCount() {
return this.users.filter(user => user.status === 'active').length
},
// 搜索结果
searchResults() {
let results = this.users
// 按搜索查询过滤
if (this.searchQuery) {
results = results.filter(user =>
user.name.toLowerCase().includes(this.searchQuery.toLowerCase())
)
}
// 按活跃状态过滤
if (this.showActiveOnly) {
results = results.filter(user => user.status === 'active')
}
return results
},
// 年龄统计
ageStats() {
const ages = this.users.map(user => user.age)
return {
min: Math.min(...ages),
max: Math.max(...ages),
avg: (ages.reduce((sum, age) => sum + age, 0) / ages.length).toFixed(1)
}
}
}
}
</script>
侦听器 (Watchers)
基础侦听器
侦听器适合在数据变化时执行异步或开销较大的操作:
vue
<template>
<div>
<h3>基础侦听器</h3>
<p>问一个问题是(Yes/No):</p>
<p>{{ question }}</p>
<input v-model="question" @input="debouncedGetAnswer">
<p v-if="loading">正在思考...</p>
<p v-else>{{ answer }}</p>
</div>
</template>
<script>
export default {
data() {
return {
question: '',
answer: '问题以 ? 结尾时我会给你答案',
loading: false
}
},
watch: {
// 侦听 question 的变化
question(newQuestion, oldQuestion) {
this.answer = '正在思考...'
this.getAnswer()
}
},
methods: {
async getAnswer() {
if (this.question.indexOf('?') === -1) {
this.answer = '问题以 ? 结尾时我会给你答案'
return
}
this.loading = true
try {
const res = await fetch('https://yesno.wtf/api')
this.answer = (await res.json()).answer
} catch (error) {
this.answer = '无法获取答案,请稍后重试'
} finally {
this.loading = false
}
},
// 防抖函数
debouncedGetAnswer() {
clearTimeout(this.timeoutId)
this.timeoutId = setTimeout(() => {
// 触发侦听器
this.question = this.question // 确保触发侦听器
}, 500)
}
}
}
</script>
深度侦听
vue
<template>
<div>
<h3>深度侦听器</h3>
<div>
<label>用户姓名: <input v-model="user.name"></label>
</div>
<div>
<label>用户年龄: <input v-model.number="user.age" type="number"></label>
</div>
<div>
<label>用户城市: <input v-model="user.address.city"></label>
</div>
<div>
<label>用户街道: <input v-model="user.address.street"></label>
</div>
<p>用户信息: {{ JSON.stringify(user, null, 2) }}</p>
<p>变更次数: {{ changeCount }}</p>
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: '张三',
age: 25,
address: {
city: '北京',
street: '朝阳区'
}
},
changeCount: 0
}
},
watch: {
// 深度侦听对象
user: {
handler(newVal, oldVal) {
console.log('用户信息发生变化:', newVal)
this.changeCount++
},
deep: true // 开启深度侦听
},
// 只侦听特定嵌套属性
'user.name'(newName, oldName) {
console.log(`姓名从 ${oldName} 变为 ${newName}`)
},
// 侦听函数返回的值
getUserInfo: {
handler(newInfo, oldInfo) {
console.log('用户信息摘要变化:', newInfo)
},
immediate: true // 立即执行
}
},
computed: {
getUserInfo() {
return `${this.user.name} - ${this.user.age}岁 - ${this.user.address.city}`
}
}
}
</script>
侦听器选项
vue
<template>
<div>
<h3>侦听器选项</h3>
<div>
<label>计数器: <input v-model.number="count" type="number"></label>
</div>
<div>
<label>延迟 (ms): <input v-model.number="delay" type="number"></label>
</div>
<p>延迟值: {{ delayedCount }}</p>
<p>历史记录: {{ history.join(', ') }}</p>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
delay: 1000,
delayedCount: 0,
history: []
}
},
watch: {
count: {
handler(newVal) {
// 使用防抖
clearTimeout(this.debounceTimer)
this.debounceTimer = setTimeout(() => {
this.delayedCount = newVal
this.history.push({
value: newVal,
timestamp: new Date()
})
// 保持历史记录在10条以内
if (this.history.length > 10) {
this.history.shift()
}
}, this.delay)
},
immediate: true // 立即以当前值触发回调
}
}
}
</script>
组合式 API 中的计算属性和侦听器
计算属性 (Composition API)
vue
<template>
<div>
<h3>组合式 API 计算属性</h3>
<div>
<input v-model="firstName" placeholder="名">
<input v-model="lastName" placeholder="姓">
</div>
<p>全名: {{ fullName }}</p>
<p>全名(大写): {{ upperCaseFullName }}</p>
<p>姓名长度: {{ nameLength }}</p>
<button @click="resetNames">重置</button>
</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('张')
const lastName = ref('三')
// 基础计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 带有 getter 和 setter 的计算属性
const upperCaseFullName = computed({
get: () => {
return fullName.value.toUpperCase()
},
set: (newValue) => {
const names = newValue.split(' ')
firstName.value = names[0] || ''
lastName.value = names[1] || ''
}
})
// 依赖多个响应式引用的计算属性
const nameLength = computed(() => {
return fullName.value.length
})
const resetNames = () => {
firstName.value = '张'
lastName.value = '三'
}
return {
firstName,
lastName,
fullName,
upperCaseFullName,
nameLength,
resetNames
}
}
}
</script>
侦听器 (Composition API)
vue
<template>
<div>
<h3>组合式 API 侦听器</h3>
<!-- 基础侦听 -->
<div>
<input v-model="searchQuery" placeholder="搜索">
<p>搜索查询: {{ searchQuery }}</p>
<p>搜索延迟值: {{ debouncedQuery }}</p>
</div>
<!-- 侦听多个源 -->
<div>
<input v-model.number="x" type="number" placeholder="X值">
<input v-model.number="y" type="number" placeholder="Y值">
<p>坐标: ({{ x }}, {{ y }})</p>
<p>距离原点: {{ distance }}</p>
</div>
<!-- 深度侦听 -->
<div>
<input v-model="user.name" placeholder="用户名">
<input v-model.number="user.age" type="number" placeholder="用户年龄">
<p>用户数据: {{ JSON.stringify(user) }}</p>
</div>
<p>状态: {{ status }}</p>
</div>
</template>
<script>
import { ref, reactive, computed, watch, watchEffect } from 'vue'
export default {
setup() {
// 基础响应式数据
const searchQuery = ref('')
const debouncedQuery = ref('')
const x = ref(0)
const y = ref(0)
const status = ref('idle')
// 响应式对象
const user = reactive({
name: '张三',
age: 25
})
// 计算属性
const distance = computed(() => {
return Math.sqrt(x.value ** 2 + y.value ** 2)
})
// 侦听单个源
watch(searchQuery, (newQuery) => {
status.value = 'searching'
clearTimeout(window.searchTimeout)
window.searchTimeout = setTimeout(() => {
debouncedQuery.value = newQuery
status.value = 'idle'
}, 500)
})
// 侦听多个源
watch([x, y], ([newX, newY], [oldX, oldY]) => {
console.log(`坐标从 (${oldX}, ${oldY}) 变为 (${newX}, ${newY})`)
status.value = 'updated'
setTimeout(() => {
status.value = 'idle'
}, 1000)
})
// 深度侦听
watch(
() => ({ ...user }), // 创建对象的浅拷贝以检测变化
(newUser, oldUser) => {
console.log('用户数据变化:', newUser, oldUser)
status.value = 'user-updated'
},
{ deep: true }
)
// 侦听器选项
watch(
() => user.name,
(newName) => {
console.log(`用户名变更为: ${newName}`)
},
{
immediate: true, // 立即执行
flush: 'post' // 组件更新后执行
}
)
// watchEffect - 自动追踪依赖
watchEffect(() => {
// 自动追踪 searchQuery 的变化
if (searchQuery.value) {
console.log('搜索查询变化:', searchQuery.value)
// 这里可以执行副作用,如API调用
}
})
// watchEffect 高级用法
const stopWatch = watchEffect(() => {
console.log(`当前距离: ${distance.value}`)
})
// 在需要时停止侦听
// stopWatch()
return {
searchQuery,
debouncedQuery,
x,
y,
distance,
user,
status
}
}
}
</script>
性能优化
计算属性的性能优势
vue
<template>
<div>
<h3>计算属性性能优化</h3>
<div>
<button @click="updateData">更新数据</button>
<button @click="forceRerender">强制重渲染</button>
</div>
<p>渲染次数: {{ renderCount }}</p>
<p>计算执行次数: {{ computedExecutionCount }}</p>
<!-- 复杂计算属性 -->
<div>
<h4>复杂计算结果:</h4>
<p v-for="(item, index) in expensiveCalculatedData" :key="index">
{{ item }}
</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
renderCount: 0,
computedExecutionCount: 0,
shouldCalculate: true,
dataSource: Array.from({ length: 1000 }, (_, i) => i),
updateTrigger: 0 // 用于强制更新
}
},
computed: {
expensiveCalculatedData() {
// 只有当依赖变化时才重新计算
this.computedExecutionCount++
if (!this.shouldCalculate) {
return []
}
// 模拟复杂计算
return this.dataSource
.filter(item => item % 2 === 0)
.map(item => `处理后的数据项: ${item * 2}`)
.slice(0, 10) // 只取前10项
}
},
methods: {
updateData() {
// 更新数据源,这会触发计算属性重新计算
this.dataSource = Array.from({ length: 1000 }, (_, i) => i + this.updateTrigger)
this.updateTrigger++
},
forceRerender() {
// 强制重渲染,但计算属性不会重新执行(因为依赖未变)
this.renderCount++
}
},
updated() {
// 每次更新都会执行,但计算属性可能不会
this.renderCount = this.renderCount // 触发更新
}
}
</script>
侦听器的性能考虑
vue
<template>
<div>
<h3>侦听器性能优化</h3>
<div>
<input v-model="inputValue" placeholder="输入内容(防抖)">
<p>实时值: {{ inputValue }}</p>
<p>防抖值: {{ debouncedValue }}</p>
</div>
<div>
<input v-model="searchTerm" placeholder="搜索(节流)">
<p>搜索项: {{ searchTerm }}</p>
</div>
<div>
<p>API调用次数: {{ apiCallCount }}</p>
<p>最后结果: {{ lastResult }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
inputValue: '',
debouncedValue: '',
searchTerm: '',
apiCallCount: 0,
lastResult: '',
debounceTimer: null,
throttleTimer: null
}
},
watch: {
inputValue: {
handler(newVal) {
// 防抖实现
clearTimeout(this.debounceTimer)
this.debounceTimer = setTimeout(() => {
this.debouncedValue = newVal
}, 500)
}
},
searchTerm: {
handler(newVal) {
// 节流实现
if (!this.throttleTimer) {
this.throttleTimer = setTimeout(() => {
this.performSearch(newVal)
this.throttleTimer = null
}, 1000)
}
}
}
},
methods: {
async performSearch(query) {
if (!query) {
this.lastResult = ''
return
}
this.apiCallCount++
// 模拟API调用
setTimeout(() => {
this.lastResult = `搜索结果: ${query} (${this.apiCallCount} 次调用)`
}, 300)
}
},
beforeUnmount() {
// 清理定时器
clearTimeout(this.debounceTimer)
clearTimeout(this.throttleTimer)
}
}
</script>
实际应用示例
表单验证
vue
<template>
<div>
<h3>表单验证示例</h3>
<form @submit.prevent="submitForm">
<div>
<label>用户名:</label>
<input v-model="form.username" :class="{ error: errors.username }">
<span v-if="errors.username" class="error">{{ errors.username }}</span>
</div>
<div>
<label>邮箱:</label>
<input v-model="form.email" :class="{ error: errors.email }">
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<div>
<label>密码:</label>
<input v-model="form.password" type="password" :class="{ error: errors.password }">
<span v-if="errors.password" class="error">{{ errors.password }}</span>
</div>
<div>
<label>确认密码:</label>
<input v-model="form.confirmPassword" type="password" :class="{ error: errors.confirmPassword }">
<span v-if="errors.confirmPassword" class="error">{{ errors.confirmPassword }}</span>
</div>
<button type="submit" :disabled="!isFormValid">提交</button>
</form>
<p>表单是否有效: {{ isFormValid ? '是' : '否' }}</p>
</div>
</template>
<script>
export default {
data() {
return {
form: {
username: '',
email: '',
password: '',
confirmPassword: ''
}
}
},
computed: {
errors() {
const errors = {}
// 用户名验证
if (!this.form.username) {
errors.username = '用户名不能为空'
} else if (this.form.username.length < 3) {
errors.username = '用户名至少需要3个字符'
}
// 邮箱验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!this.form.email) {
errors.email = '邮箱不能为空'
} else if (!emailRegex.test(this.form.email)) {
errors.email = '邮箱格式不正确'
}
// 密码验证
if (!this.form.password) {
errors.password = '密码不能为空'
} else if (this.form.password.length < 6) {
errors.password = '密码至少需要6个字符'
}
// 确认密码验证
if (!this.form.confirmPassword) {
errors.confirmPassword = '请确认密码'
} else if (this.form.password !== this.form.confirmPassword) {
errors.confirmPassword = '密码不匹配'
}
return errors
},
isFormValid() {
// 如果没有错误,表单有效
return Object.keys(this.errors).length === 0
}
},
methods: {
submitForm() {
if (this.isFormValid) {
console.log('表单提交:', this.form)
alert('表单提交成功!')
} else {
console.log('表单验证失败:', this.errors)
}
}
}
}
</script>
<style scoped>
.error {
color: red;
font-size: 0.9em;
}
input.error {
border: 1px solid red;
}
form div {
margin-bottom: 15px;
}
</style>
数据过滤和排序
vue
<template>
<div>
<h3>数据过滤和排序</h3>
<div class="controls">
<input v-model="searchQuery" placeholder="搜索...">
<select v-model="sortField">
<option value="name">按姓名排序</option>
<option value="age">按年龄排序</option>
<option value="salary">按薪资排序</option>
</select>
<select v-model="sortOrder">
<option value="asc">升序</option>
<option value="desc">降序</option>
</select>
<select v-model="filterByAge">
<option value="">所有年龄</option>
<option value="young">年轻人(<30)</option>
<option value="middle">中年人(30-50)</option>
<option value="old">老年人(>50)</option>
</select>
</div>
<p>显示 {{ filteredAndSortedEmployees.length }} / {{ employees.length }} 个员工</p>
<table>
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>部门</th>
<th>薪资</th>
</tr>
</thead>
<tbody>
<tr v-for="employee in filteredAndSortedEmployees" :key="employee.id">
<td>{{ employee.name }}</td>
<td>{{ employee.age }}</td>
<td>{{ employee.department }}</td>
<td>{{ employee.salary }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: '',
sortField: 'name',
sortOrder: 'asc',
filterByAge: '',
employees: [
{ id: 1, name: '张三', age: 28, department: '技术部', salary: 15000 },
{ id: 2, name: '李四', age: 35, department: '销售部', salary: 12000 },
{ id: 3, name: '王五', age: 42, department: '人事部', salary: 10000 },
{ id: 4, name: '赵六', age: 25, department: '技术部', salary: 13000 },
{ id: 5, name: '钱七', age: 31, department: '财务部', salary: 11000 },
{ id: 6, name: '孙八', age: 48, department: '管理部', salary: 18000 }
]
}
},
computed: {
filteredEmployees() {
let result = this.employees
// 搜索过滤
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase()
result = result.filter(employee =>
employee.name.toLowerCase().includes(query) ||
employee.department.toLowerCase().includes(query)
)
}
// 年龄过滤
if (this.filterByAge) {
switch (this.filterByAge) {
case 'young':
result = result.filter(emp => emp.age < 30)
break
case 'middle':
result = result.filter(emp => emp.age >= 30 && emp.age <= 50)
break
case 'old':
result = result.filter(emp => emp.age > 50)
break
}
}
return result
},
sortedEmployees() {
const result = [...this.filteredEmployees]
// 排序
result.sort((a, b) => {
let modifier = 1
if (this.sortOrder === 'desc') {
modifier = -1
}
if (a[this.sortField] < b[this.sortField]) return -1 * modifier
if (a[this.sortField] > b[this.sortField]) return 1 * modifier
return 0
})
return result
},
filteredAndSortedEmployees() {
// 组合过滤和排序
return this.sortedEmployees
}
}
}
</script>
<style scoped>
.controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
计算属性和侦听器是Vue.js中处理数据派生和副作用的强大工具,正确使用它们可以提高应用性能和可维护性。