深入理解Vue3响应式系统的核心机制:计算属性缓存与数据变化侦听
计算属性是基于响应式依赖进行缓存的派生值,只有当依赖发生变化时才会重新计算。
缓存机制:依赖追踪 + 惰性求值
<template>
<div>
<h3>用户信息管理</h3>
<div>
<label>名: <input v-model="firstName"></label>
<label>姓: <input v-model="lastName"></label>
</div>
<div>
<label>全名(可编辑): <input v-model="fullName"></label>
</div>
<div>
<p>原始消息: {{ message }}</p>
<p>反转消息: {{ reversedMessage }}</p>
<p>全名: {{ fullName }}</p>
<p>姓名长度: {{ nameLength }} 字符</p>
<p>格式化姓名: {{ formattedName }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue3',
firstName: '张',
lastName: '三'
}
},
computed: {
// 只读计算属性:消息反转
reversedMessage() {
console.log('计算属性 reversedMessage 执行')
return this.message.split('').reverse().join('')
},
// 可读写计算属性:全名
fullName: {
get() {
console.log('计算属性 fullName getter 执行')
return this.firstName + ' ' + this.lastName
},
set(newValue) {
console.log('计算属性 fullName setter 执行,新值:', newValue)
const names = newValue.split(' ')
this.firstName = names[0] || ''
this.lastName = names[names.length - 1] || ''
}
},
// 只读计算属性:姓名长度
nameLength() {
return (this.firstName + this.lastName).length
},
// 只读计算属性:格式化姓名
formattedName() {
return `${this.lastName} ${this.firstName}`.toUpperCase()
}
},
mounted() {
// 演示计算属性缓存:多次访问只计算一次
console.log('第一次访问 reversedMessage:', this.reversedMessage)
console.log('第二次访问 reversedMessage:', this.reversedMessage)
console.log('第三次访问 reversedMessage:', this.reversedMessage)
}
}
</script>
<template>
<div>
<h3>组合式API计算属性演示</h3>
<div>
<label>商品单价: <input v-model.number="price" type="number"></label>
<label>购买数量: <input v-model.number="quantity" type="number"></label>
</div>
<div>
<h4>计算结果</h4>
<p>商品总价: {{ totalPrice }} 元</p>
<p>折扣后价格: {{ discountedPrice }} 元</p>
<p>税费: {{ taxAmount }} 元</p>
<p>最终价格: {{ finalPrice }} 元</p>
<p>是否享受优惠: {{ isEligibleForDiscount ? '是' : '否' }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const price = ref(100)
const quantity = ref(2)
const discountRate = ref(0.1) // 10%折扣
const taxRate = ref(0.08) // 8%税费
// 计算属性:商品总价
const totalPrice = computed(() => {
console.log('计算属性 totalPrice 执行')
return price.value * quantity.value
})
// 计算属性:折扣后价格
const discountedPrice = computed(() => {
return totalPrice.value * (1 - discountRate.value)
})
// 计算属性:税费
const taxAmount = computed(() => {
return discountedPrice.value * taxRate.value
})
// 计算属性:最终价格(依赖其他计算属性)
const finalPrice = computed(() => {
return discountedPrice.value + taxAmount.value
})
// 计算属性:是否享受优惠
const isEligibleForDiscount = computed(() => {
return totalPrice.value > 150
})
// 演示计算属性缓存
console.log('第一次访问 totalPrice:', totalPrice.value)
console.log('第二次访问 totalPrice:', totalPrice.value)
console.log('第三次访问 totalPrice:', totalPrice.value)
// 当价格变化时,观察计算属性的重新计算
price.value = 200
console.log('价格变化后访问 totalPrice:', totalPrice.value)
</script>
<template>
<div>
<h3>性能对比:计算属性 vs 方法</h3>
<div>
<label>消息: <input v-model="message"></label>
<button @click="triggerRerender">触发重新渲染</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
<div style="border: 2px solid #42b883; padding: 15px; border-radius: 8px;">
<h4>计算属性(有缓存)</h4>
<p>{{ reversedMessage }}</p>
<p>{{ reversedMessage }}</p>
<p>{{ reversedMessage }}</p>
<p>执行次数: {{ computedCount }}</p>
</div>
<div style="border: 2px solid #ff6b6b; padding: 15px; border-radius: 8px;">
<h4>方法(无缓存)</h4>
<p>{{ reverseMessage() }}</p>
<p>{{ reverseMessage() }}</p>
<p>{{ reverseMessage() }}</p>
<p>执行次数: {{ methodCount }}</p>
</div>
</div>
<div style="margin-top: 20px;">
<p><strong>结论:</strong>计算属性在相同依赖下只计算一次,方法每次调用都执行</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const message = ref('Hello Vue3')
const computedCount = ref(0)
const methodCount = ref(0)
// 计算属性:有缓存
const reversedMessage = computed(() => {
computedCount.value++
console.log('计算属性执行,次数:', computedCount.value)
return message.value.split('').reverse().join('')
})
// 方法:无缓存
function reverseMessage() {
methodCount.value++
console.log('方法执行,次数:', methodCount.value)
return message.value.split('').reverse().join('')
}
// 触发重新渲染
function triggerRerender() {
// 重置计数器
computedCount.value = 0
methodCount.value = 0
// 强制重新渲染(通过修改无关数据)
message.value = message.value + '!'
}
</script>
| 特性 | 计算属性 | 方法 |
|---|---|---|
| 缓存机制 | ✅ 有缓存,依赖不变不重新计算 | ❌ 每次调用都执行 |
| 性能 | ✅ 高性能,适合复杂计算 | ⚠️ 性能取决于调用频率 |
| 响应式 | ✅ 自动追踪依赖 | ❌ 需要手动处理依赖 |
| 适用场景 | 派生数据、格式化、过滤 | 事件处理、异步操作 |
| 语法 | 属性访问(无括号) | 方法调用(有括号) |
侦听器用于观察和响应数据的变化,适合执行异步操作或开销较大的操作。
<template>
<div>
<input v-model="question" placeholder="输入问题">
<p>{{ answer }}</p>
</div>
</template>
<script>
export default {
data() {
return {
question: '',
answer: '请输入问题'
}
},
watch: {
// 简单侦听
question(newValue, oldValue) {
console.log(`问题从 "${oldValue}" 变为 "${newValue}"`)
this.getAnswer()
},
// 深度侦听
user: {
handler(newValue, oldValue) {
console.log('用户信息变化')
},
deep: true,
immediate: true
}
},
methods: {
getAnswer() {
if (this.question.includes('?')) {
this.answer = '思考中...'
setTimeout(() => {
this.answer = '这是一个好问题!'
}, 1000)
}
}
}
}
</script>
<template>
<div>
<input v-model="question" placeholder="输入问题">
<p>{{ answer }}</p>
</div>
</template>
<script setup>
import { ref, watch, watchEffect } from 'vue'
const question = ref('')
const answer = ref('请输入问题')
// watch: 明确指定侦听源
watch(question, (newValue, oldValue) => {
console.log(`问题从 "${oldValue}" 变为 "${newValue}"`)
if (newValue.includes('?')) {
answer.value = '思考中...'
setTimeout(() => {
answer.value = '这是一个好问题!'
}, 1000)
}
})
// watchEffect: 自动追踪依赖
watchEffect(() => {
console.log(`当前问题: ${question.value}`)
})
// 侦听多个源
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名变化: ${newFirst} ${newLast}`)
})
</script>
<script setup>
import { ref, watch } from 'vue'
const user = ref({
name: '张三',
age: 25,
address: {
city: '北京'
}
})
// 深度侦听对象
watch(user, (newValue, oldValue) => {
console.log('用户信息变化', newValue)
}, { deep: true })
// 侦听对象的某个属性
watch(() => user.value.name, (newValue, oldValue) => {
console.log(`姓名从 ${oldValue} 变为 ${newValue}`)
})
</script>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// immediate: 立即执行一次
watch(count, (newValue) => {
console.log('count变化:', newValue)
}, { immediate: true })
// watchEffect: 默认立即执行
watchEffect(() => {
console.log('count当前值:', count.value)
})
</script>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 侦听器返回停止函数
const stopWatch = watch(count, (newValue) => {
console.log('count:', newValue)
if (newValue >= 10) {
stopWatch() // 停止侦听
}
})
</script>
<template>
<div>
<input v-model="searchText" placeholder="搜索...">
<p>搜索结果数量: {{ filteredItems.length }}</p>
<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const searchText = ref('')
const items = ref([
{ id: 1, name: 'Vue3教程' },
{ id: 2, name: 'React教程' },
{ id: 3, name: 'Vue Router' },
{ id: 4, name: 'Pinia状态管理' }
])
// 使用计算属性过滤
const filteredItems = computed(() => {
return items.value.filter(item =>
item.name.toLowerCase().includes(searchText.value.toLowerCase())
)
})
// 使用侦听器记录搜索历史
watch(searchText, (newValue) => {
if (newValue) {
console.log('搜索:', newValue)
// 可以在这里保存搜索历史
}
})
</script>