Skip to content

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 自动追踪依赖
  • 计算属性适合简单的数据转换,侦听器适合复杂的异步操作

下一步

接下来学习 条件渲染,了解如何根据条件显示或隐藏元素。