<返回目录     Powered by claud/xia兄

第3课: 计算属性与侦听器

深入理解Vue3响应式系统的核心机制:计算属性缓存与数据变化侦听

📚 学习目标

🎯 计算属性(Computed)深度解析

计算属性是基于响应式依赖进行缓存的派生值,只有当依赖发生变化时才会重新计算。

🔬 计算属性工作原理

缓存机制:依赖追踪 + 惰性求值

响应式依赖追踪流程

  1. 依赖收集:计算属性首次执行时,收集所有访问的响应式数据
  2. 缓存结果:计算结果被缓存,避免重复计算
  3. 依赖变化检测:当依赖数据变化时,标记计算属性为"脏"
  4. 惰性重新计算:下次访问时重新计算并更新缓存
技术原理:Vue3使用Proxy的get拦截器来追踪依赖关系,通过effect作用域管理计算属性的生命周期。

Options API写法

<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>
计算属性特点:

Composition API写法

<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>

💡 计算属性最佳实践

何时使用计算属性

性能优化技巧

⚖️ 计算属性 vs 方法:性能对比

🔍 性能对比演示

<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>

📊 选择指南:计算属性 vs 方法

特性计算属性方法
缓存机制✅ 有缓存,依赖不变不重新计算❌ 每次调用都执行
性能✅ 高性能,适合复杂计算⚠️ 性能取决于调用频率
响应式✅ 自动追踪依赖❌ 需要手动处理依赖
适用场景派生数据、格式化、过滤事件处理、异步操作
语法属性访问(无括号)方法调用(有括号)

使用建议

侦听器(Watch)

侦听器用于观察和响应数据的变化,适合执行异步操作或开销较大的操作。

Options API写法

<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>

Composition API写法

<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>

练习

  1. 创建一个购物车,使用计算属性计算总价
  2. 实现一个表单验证,使用侦听器检查输入是否合法
  3. 创建一个实时搜索功能,使用计算属性过滤列表
  4. 使用watchEffect实现一个自动保存功能