阅读视图

发现新文章,点击刷新页面。

Vue3计算属性如何通过缓存特性优化表单验证与数据过滤?

在日常开发中,表单验证和动态数据过滤几乎是每个项目都会遇到的需求——比如用户注册时要检查输入是否合法,商品列表要根据关键词实时筛选。这两个场景看似简单,但处理不好容易写出冗余、低效的代码。而Vue3的**计算属性(Computed Properties)**正好是解决这类问题的“神器”,今天我们就通过实战案例,看看它如何简化状态管理和逻辑复用。

一、表单验证:用计算属性简化状态管理

1.1 为什么用计算属性做表单验证?

做表单验证时,我们需要判断“所有字段是否合法”——比如用户名不能为空、密码至少6位、确认密码要一致。如果用methods写,每次模板渲染都会重新调用方法,哪怕字段没变化;而计算属性会缓存结果,只有当依赖的响应式数据(比如form.username)变化时,才会重新计算。这样既省性能,又让代码更简洁。

Vue官网是这么说的:“计算属性基于它们的依赖进行缓存。只在相关依赖发生改变时才会重新求值。”(参考:vuejs.org/guide/essen…

1.2 实战:用户注册表单验证

我们来写一个用户注册表单,用计算属性判断表单是否可以提交。代码如下:

<template>
  <form class="register-form" @submit.prevent="handleSubmit">
    <!-- 用户名输入框 -->
    <div class="form-group">
      <label>用户名:</label>
      <input 
        v-model.trim="form.username" 
        placeholder="请输入3-10位字符" 
        class="form-input"
      />
      <!-- 错误提示 -->
      <p v-if="!form.username" class="error-msg">用户名不能为空</p>
    </div>

    <!-- 密码输入框 -->
    <div class="form-group">
      <label>密码:</label>
      <input 
        type="password" 
        v-model="form.password" 
        placeholder="请输入6-16位密码" 
        class="form-input"
      />
      <p v-if="form.password.length < 6" class="error-msg">密码至少6位</p>
    </div>

    <!-- 确认密码 -->
    <div class="form-group">
      <label>确认密码:</label>
      <input 
        type="password" 
        v-model="form.confirmPassword" 
        placeholder="请再次输入密码" 
        class="form-input"
      />
      <p v-if="form.confirmPassword !== form.password" class="error-msg">两次密码不一致</p>
    </div>

    <!-- 提交按钮:禁用状态由计算属性控制 -->
    <button 
      type="submit" 
      class="submit-btn" 
      :disabled="!formIsValid"
    >
      提交注册
    </button>
  </form>
</template>

<script setup>
import { ref, computed } from 'vue'

// 1. 响应式表单数据
const form = ref({
  username: '',   // 用户名
  password: '',   // 密码
  confirmPassword: ''  // 确认密码
})

// 2. 计算属性:判断表单是否合法
const formIsValid = computed(() => {
  // 解构form数据,简化代码
  const { username, password, confirmPassword } = form.value
  // 验证逻辑:用户名非空 + 密码≥6位 + 两次密码一致
  return (
    username.trim() !== '' &&  // 去掉空格后非空
    password.length >= 6 &&    // 密码长度足够
    confirmPassword === password  // 确认密码一致
  )
})

// 3. 提交事件处理
const handleSubmit = () => {
  if (formIsValid.value) {
    alert('注册成功!');
    // 这里可以加向后端提交数据的逻辑,比如axios.post('/api/register', form.value)
  }
}
</script>

<style scoped>
.register-form { max-width: 400px; margin: 20px auto; }
.form-group { margin-bottom: 15px; }
.form-input { width: 100%; padding: 8px; margin-top: 5px; }
.error-msg { color: red; font-size: 12px; margin: 5px 0 0 0; }
.submit-btn { width: 100%; padding: 10px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
.submit-btn:disabled { background: #ccc; cursor: not-allowed; }
</style>

代码解释:

  • 响应式数据:用ref包裹表单对象form,这样输入时form的属性会自动更新。
  • 计算属性formIsValid:依赖form的三个属性,当其中任何一个变化时,自动重新计算“表单是否合法”。
  • 禁用按钮:用:disabled="!formIsValid"绑定按钮状态——只有formIsValidtrue时,按钮才能点击。
  • 提交逻辑handleSubmit里先判断formIsValid.value,确保提交的是合法数据。

1.3 流程图:表单验证的计算属性逻辑

为了更直观,我们用流程图展示计算属性的工作流程:

flowchart LR
A[用户输入表单内容] --> B[form数据响应式更新]
B --> C[计算属性formIsValid重新计算]
C --> D{formIsValid是否为true?}
D -->|是| E[提交按钮可用,允许提交]
D -->|否| F[提交按钮禁用,提示错误]

二、动态数据过滤:计算属性的缓存魔法

2.1 为什么用计算属性做动态过滤?

另一个常见场景是动态数据过滤——比如商品列表,用户输入关键词后,实时显示包含关键词的商品。这时计算属性的缓存特性就很有用:只有当搜索关键词(searchQuery)或商品列表(products)变化时,才会重新过滤,避免不必要的重复计算。

2.2 实战:商品列表动态过滤

我们来写一个商品列表,用计算属性实现实时过滤:

<template>
  <div class="product-filter">
    <!-- 搜索输入框 -->
    <input 
      v-model.trim="searchQuery" 
      placeholder="搜索商品名称" 
      class="search-input"
    />

    <!-- 过滤后的商品列表 -->
    <ul class="product-list">
      <li 
        v-for="product in filteredProducts" 
        :key="product.id" 
        class="product-item"
      >
        {{ product.name }} - {{ product.price }}元
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

// 1. 模拟后端获取的商品数据(响应式)
const products = ref([
  { id: 1, name: 'Vue3实战教程', price: 99 },
  { id: 2, name: 'React入门指南', price: 79 },
  { id: 3, name: 'JavaScript进阶', price: 129 },
  { id: 4, name: 'Vue3组件库', price: 59 }
])

// 2. 搜索关键词(响应式)
const searchQuery = ref('')

// 3. 计算属性:过滤后的商品列表
const filteredProducts = computed(() => {
  // 统一转为小写,避免大小写问题
  const query = searchQuery.value.toLowerCase()
  // 过滤逻辑:商品名称包含关键词
  return products.value.filter(product => {
    return product.name.toLowerCase().includes(query)
  })
})
</script>

<style scoped>
.product-filter { max-width: 600px; margin: 20px auto; }
.search-input { width: 100%; padding: 10px; margin-bottom: 15px; }
.product-list { list-style: none; padding: 0; }
.product-item { padding: 10px; border-bottom: 1px solid #eee; }
</style>

代码解释:

  • 响应式数据products是商品列表(模拟后端数据),searchQuery是用户输入的关键词。
  • 计算属性filteredProducts:依赖searchQueryproducts,当其中任何一个变化时,重新过滤商品列表。
  • 过滤逻辑:用filter方法筛选出名称包含关键词的商品,toLowerCase()统一大小写,避免“Vue”和“vue”不匹配的问题。

2.3 流程图:动态过滤的计算属性流程

flowchart LR
A[用户输入搜索关键词] --> B[searchQuery响应式更新]
B --> C[计算属性filteredProducts重新计算]
C --> D[过滤products列表]
D --> E[渲染过滤后的商品列表]

三、计算属性的进阶技巧:组合逻辑复用

3.1 抽取可复用的验证逻辑

在表单验证中,我们可能需要复用逻辑——比如“密码强度检查”,多个表单都需要判断密码是否包含大小写字母和数字。这时可以把逻辑抽成可组合函数(Composable),让代码更简洁、可复用。

往期文章归档
免费好用的热门在线工具

示例:抽取密码强度验证

我们创建一个usePasswordStrength.js文件,封装密码强度检查逻辑:

// composables/usePasswordStrength.js
import { computed } from 'vue'

/**
 * 密码强度检查的可组合函数
 * @param {Ref<string>} passwordRef - 密码的响应式引用
 * @returns {Object} 包含密码强度的计算属性
 */
export function usePasswordStrength(passwordRef) {
  // 计算属性:密码强度
  const passwordStrength = computed(() => {
    const password = passwordRef.value
    if (password.length === 0) return '请输入密码'
    if (password.length < 6) return '弱(至少6位)'
    // 检查是否包含小写、大写、数字
    const hasLower = /[a-z]/.test(password)
    const hasUpper = /[A-Z]/.test(password)
    const hasNumber = /\d/.test(password)
    // 强度等级:3项都满足→强,2项→中,1项→弱
    const strengthCount = [hasLower, hasUpper, hasNumber].filter(Boolean).length
    if (strengthCount === 3) return '强'
    if (strengthCount === 2) return '中'
    return '弱'
  })

  return { passwordStrength }
}

然后在注册表单中使用这个函数:

<template>
  <!-- 密码输入框 -->
  <div class="form-group">
    <label>密码:</label>
    <input type="password" v-model="form.password" class="form-input" />
    <p class="strength-msg">密码强度:{{ passwordStrength }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { usePasswordStrength } from '@/composables/usePasswordStrength'

const form = ref({ password: '' })
// 使用可组合函数,传入密码的响应式引用
const { passwordStrength } = usePasswordStrength(() => form.value.password)
</script>

<style scoped>
.strength-msg { font-size: 12px; margin: 5px 0 0 0; }
.strength-msg:contains('弱') { color: #f56c6c; }
.strength-msg:contains('中') { color: #e6a23c; }
.strength-msg:contains('强') { color: #67c23a; }
</style>

这样,不管多少个表单需要密码强度检查,只需引入usePasswordStrength即可,大大提高了代码的复用性!

四、课后Quiz:巩固所学知识

Quiz1:在动态数据过滤的示例中,如果把computed换成methods,会有什么区别?为什么?

答案解析

  • 区别computed会缓存结果,只有依赖的响应式数据(searchQueryproducts)变化时才重新计算;methods每次组件渲染都会重新调用,即使依赖的数据没变化。
  • 原因:比如用户输入关键词后,searchQuery变化,computed会重新过滤一次;但如果页面上有其他变化(比如时间戳更新),methods会再次调用filter方法,而computed不会——因为它的依赖没变化。
  • 结论computed更适合“衍生状态”(由其他数据推导而来),methods更适合“执行动作”(比如点击事件)。参考:vuejs.org/guide/essen…

Quiz2:表单验证中的formIsValid,为什么不用watch来实现?

答案解析

  • watch是“观察数据变化并执行副作用”(比如异步请求、DOM操作),而computed是“推导新的响应式数据”。
  • 如果用watch实现formIsValid,需要手动维护一个isValid变量:
    const formIsValid = ref(false)
    watch([() => form.value.username, () => form.value.password, () => form.value.confirmPassword], () => {
      formIsValid.value = /* 验证逻辑 */
    })
    
  • 相比之下,computed更简洁:const formIsValid = computed(() => /* 验证逻辑 */),而且自动缓存结果。
  • 结论:computed是“声明式”的(告诉Vue“我要什么”),watch是“命令式”的(告诉Vue“要做什么”)。参考:vuejs.org/guide/essen…

五、常见报错及解决方案

1. 报错:“Computed property "formIsValid" was assigned to but it has no setter.”

  • 原因:试图给computed属性赋值(比如formIsValid = true),但computed默认是只读的(除非定义setter)。
  • 解决:不要直接修改computed属性,而是修改它依赖的响应式数据(比如form.value.username = 'admin')。如果需要可写的computed,可以定义gettersetter
    const fullName = computed({
      get() { return this.firstName + ' ' + this.lastName },
      set(value) {
        [this.firstName, this.lastName] = value.split(' ')
      }
    })
    
  • 预防:记住computed是“衍生状态”,修改依赖的数据即可,不要直接赋值。

2. 报错:“Property "formIsValid" was accessed during render but is not defined on instance.”

  • 原因:模板中用了formIsValid,但script中没有定义,或者定义错误(比如写成了methods里的函数)。
  • 解决:检查script中是否正确定义了computed属性:const formIsValid = computed(...),并且script setup会自动导出顶层变量(不需要export)。
  • 预防:写模板时同步修改script,确保变量名一致。

3. 报错:“Invalid watch source: 5 A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these.”

  • 原因watch的源不是响应式数据或函数(比如watch(5, () => {}))。
  • 解决:确保watch的源是响应式的,比如:
    watch(() => form.value.username, () => { /* 逻辑 */ }) // getter函数
    watch(searchQuery, () => { /* 逻辑 */ }) // ref变量
    
  • 预防:使用watch时,源要指向响应式数据的getter函数或ref/reactive对象。

参考链接

❌