Vue3 computed计算属性

一、computed计算属性基本概念

1.1 什么是computed计算属性?

computed计算属性是Vue3提供的用于声明式地依赖响应式状态的派生值的API。它允许你定义一个依赖于其他响应式数据的属性,当依赖的数据变化时,计算属性会自动更新。

核心特点

  • 响应式依赖:自动追踪其依赖的响应式数据
  • 缓存机制:只有当依赖变化时才会重新计算
  • 声明式语法:以声明式方式描述派生值,更简洁易读
  • 只读默认:默认情况下是只读的,可配置为可写

生活比喻:计算属性就像一个自动更新的公式。例如,当你计算"总分=语文+数学+英语"时,只要任何一门成绩变化,总分会自动更新,而不需要每次手动计算。

核心区别总结

特性 computed计算属性 方法
执行时机 依赖变化时自动执行 每次调用时执行
缓存 有缓存,依赖不变不重新计算 无缓存,每次调用都执行
调用方式 作为属性访问(无括号) 作为方法调用(有括号)
参数 不能接收参数 可以接收参数
适用场景 依赖固定的响应式数据派生值 需要参数或频繁变化的计算

1.3 computed计算属性的基本使用

<template>
  <div class="computed-basic">
    <h2>计算属性基本使用</h2>
    
    <div class="input-section">
      <label>: <input v-model="firstName" placeholder="请输入姓">
      </label>
      <label>: <input v-model="lastName" placeholder="请输入名">
      </label>
    </div>
    
    <div class="result-section">
      <p><strong>全名:</strong> {{ fullName }}</p>
      <p><strong>全名(大写):</strong> {{ fullNameUpperCase }}</p>
      <p><strong>姓名长度:</strong> {{ nameLength }} 个字符</p>
    </div>
  </div>
</template>

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

// 基本响应式数据
const firstName = ref('');
const lastName = ref('');

// 计算属性 - 全名
const fullName = computed(() => {
  // 当firstName或lastName变化时,会重新计算
  return `${firstName.value} ${lastName.value}`.trim() || '请输入姓名';
});

// 计算属性 - 全名大写
const fullNameUpperCase = computed(() => {
  // 依赖于fullName计算属性
  return fullName.value.toUpperCase();
});

// 计算属性 - 姓名长度
const nameLength = computed(() => {
  // 依赖于fullName计算属性
  return fullName.value.length;
});
</script>

<style scoped>
</style>

在这里插入图片描述

运行结果

  • 在姓和名输入框中输入内容,全名会实时更新
  • 全名大写和姓名长度也会自动更新
  • 如果未输入内容,显示"请输入姓名"

代码解析

  • 使用computed()函数创建计算属性,参数是一个 getter 函数
  • 计算属性的值基于 getter 函数的返回值
  • 计算属性可以依赖其他计算属性(如fullNameUpperCase依赖fullName
  • 计算属性会自动追踪其依赖的响应式数据,当依赖变化时重新计算

二、computed计算属性的高级特性

2.1 可写计算属性

默认情况下,计算属性是只读的。但可以通过提供一个gettersetter函数,创建可写的计算属性。

<template>
  <div class="writable-computed">
    <h2>可写计算属性</h2>
    
    <div class="input-group">
      <label>姓: <input v-model="firstName"></label>
    </div>
    
    <div class="input-group">
      <label>名: <input v-model="lastName"></label>
    </div>
    
    <div class="input-group">
      <label>全名: <input v-model="fullName"></label>
    </div>
    
    <div class="log-section">
      <h3>操作日志:</h3>
      <ul>
        <li v-for="(log, index) in logs" :key="index" :class="{ recent: index === logs.length - 1 }">
          {{ log }}
        </li>
      </ul>
    </div>
  </div>
</template>

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

// 基本数据
const firstName = ref('张');
const lastName = ref('三');
const logs = ref([]);

// 添加日志
const addLog = (message) => {
  logs.value.push(`[${new Date().toLocaleTimeString()}] ${message}`);
  // 只保留最近10条日志
  if (logs.value.length > 10) {
    logs.value.shift();
  }
};

// 可写计算属性
const fullName = computed({
  // getter - 读取时调用
  get() {
    const name = `${firstName.value} ${lastName.value}`.trim();
    addLog('读取全名: ' + name);
    return name;
  },
  
  // setter - 修改时调用
  set(newValue) {
    addLog('修改全名: ' + newValue);
    
    // 将新值拆分为名和姓
    const names = newValue.split(' ');
    
    if (names.length >= 2) {
      firstName.value = names[0];
      lastName.value = names.slice(1).join(' ');
    } else if (names.length === 1) {
      firstName.value = names[0];
      lastName.value = '';
    } else {
      firstName.value = '';
      lastName.value = '';
    }
  }
});

// 初始日志
addLog('组件初始化');
</script>

<style scoped>

</style>

在这里插入图片描述

运行结果

  • 修改姓或名输入框,全名输入框会自动更新
  • 修改全名输入框,姓和名输入框会自动拆分更新
  • 操作日志会记录所有读取和修改操作

代码解析

  • 可写计算属性通过传递一个包含getset方法的对象创建
  • get方法:读取计算属性时调用,返回计算后的值
  • set方法:修改计算属性时调用,接收新值,可以反向更新依赖的响应式数据
  • 可写计算属性适用于需要双向绑定的派生值场景

2.4 计算属性与watch的区别

计算属性和watch都可以响应数据变化,但它们的适用场景不同。

<template>
  <div class="computed-vs-watch">
    <h2>计算属性 vs watch</h2>
    
    <div class="input-section">
      <label>
        输入: <input v-model="inputValue" placeholder="输入一些文本">
      </label>
    </div>
    
    <div class="results">
      <div class="result-item">
        <h3>计算属性结果</h3>
        <p>字符数: {{ charCount }}</p>
        <p>单词数: {{ wordCount }}</p>
        <p>大写转换: {{ upperCaseText }}</p>
      </div>
      
      <div class="result-item">
        <h3>watch结果</h3>
        <p>最后修改时间: {{ lastUpdated }}</p>
        <p>历史字符数: {{ historyCharCounts.join(', ') }}</p>
        <p>变化次数: {{ changeCount }}</p>
      </div>
    </div>
  </div>
</template>

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

// 基本数据
const inputValue = ref('');
const lastUpdated = ref('');
const historyCharCounts = ref([]);
const changeCount = ref(0);

// 计算属性 - 适合派生值
const charCount = computed(() => {
  return inputValue.value.length;
});

const wordCount = computed(() => {
  if (!inputValue.value.trim()) return 0;
  return inputValue.value.trim().split(/\s+/).length;
});

const upperCaseText = computed(() => {
  return inputValue.value.toUpperCase();
});

// watch - 适合执行副作用
watch(inputValue, (newValue, oldValue) => {
  // 记录最后修改时间
  lastUpdated.value = new Date().toLocaleTimeString();
  
  // 记录历史字符数
  historyCharCounts.value.push(newValue.length);
  
  // 限制历史记录长度
  if (historyCharCounts.value.length > 5) {
    historyCharCounts.value.shift();
  }
  
  // 增加变化次数
  changeCount.value++;
});
</script>

<style scoped>
.computed-vs-watch {
  max-width: 800px;
  margin: 20px auto;
  padding: 20px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
}

.input-section {
  margin-bottom: 20px;
}

input {
  padding: 8px;
  width: 300px;
}

.results {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
}

.result-item {
  flex: 1;
  min-width: 300px;
  padding: 15px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
}
</style>

运行结果

  • 在输入框中输入文本:
    • 计算属性实时更新字符数、单词数和大写转换结果
    • watch记录最后修改时间、历史字符数和变化次数

核心区别总结

特性 computed计算属性 watch
用途 声明式地派生值 执行响应数据变化的副作用
语法 函数式,返回计算值 命令式,执行函数
缓存 有缓存 无缓存
返回值 必须有返回值 不需要返回值
适用场景 数据转换、过滤、组合 数据变化后的异步操作、复杂逻辑
依赖 自动追踪响应式依赖 需要显式指定依赖

三、computed计算属性的实际应用场景

3.1 数据转换和格式化

计算属性非常适合对原始数据进行转换和格式化,如日期格式化、货币格式化等。

<template>
  <div class="data-formatting">
    <h2>数据转换和格式化</h2>
    
    <div class="data-input">
      <label>
        原始价格: <input v-model.number="price" type="number" step="0.01" min="0">
      </label>
      <label>
        原始日期: <input v-model="rawDate" type="date">
      </label>
    </div>
    
    <div class="formatted-results">
      <p><strong>格式化价格:</strong> {{ formattedPrice }}</p>
      <p><strong>带符号价格:</strong> {{ priceWithSymbol }}</p>
      <p><strong>格式化日期:</strong> {{ formattedDate }}</p>
      <p><strong>相对日期:</strong> {{ relativeDate }}</p>
    </div>
  </div>
</template>

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

// 原始数据
const price = ref(1234.56);
const rawDate = ref('2023-01-15');

// 格式化价格
const formattedPrice = computed(() => {
  return new Intl.NumberFormat('zh-CN', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(price.value);
});

// 带符号价格
const priceWithSymbol = computed(() => {
  return new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY'
  }).format(price.value);
});

// 格式化日期
const formattedDate = computed(() => {
  if (!rawDate.value) return '';
  return new Intl.DateTimeFormat('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    weekday: 'long'
  }).format(new Date(rawDate.value));
});

// 相对日期(如"3天前")
const relativeDate = computed(() => {
  if (!rawDate.value) return '';
  
  const today = new Date();
  const inputDate = new Date(rawDate.value);
  const diffTime = today - inputDate;
  const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
  
  if (diffDays === 0) return '今天';
  if (diffDays === 1) return '昨天';
  if (diffDays === -1) return '明天';
  if (diffDays > 0) return `${diffDays}天前`;
  return `${Math.abs(diffDays)}天后`;
});
</script>

<style scoped>
.data-formatting {
  max-width: 600px;
  margin: 20px auto;
  padding: 20px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
}

.data-input {
  display: flex;
  flex-direction: column;
  gap: 15px;
  margin-bottom: 20px;
}

input {
  padding: 8px;
  width: 250px;
}

.formatted-results {
  padding: 15px;
  background-color: #f5f5f5;
  border-radius: 4px;
}

p {
  margin: 8px 0;
}
</style>

在这里插入图片描述

运行结果

  • 价格输入框输入1234.56,显示:
    • 格式化价格: 1,234.56
    • 带符号价格: ¥1,234.56
  • 日期选择2023-01-15,显示:
    • 格式化日期: 2023年1月15日 星期日
    • 相对日期: 根据当前日期计算的相对天数

代码解析

  • 使用计算属性对价格和日期进行格式化
  • 利用浏览器内置的Intl对象进行国际化格式化
  • 计算属性使数据转换逻辑与模板分离,更易于维护
  • 当原始数据变化时,格式化结果会自动更新

3.2 数据过滤和搜索

计算属性非常适合实现数据过滤和搜索功能,尤其是当数据量不大时。

<template>
  <div class="data-filtering">
    <h2>数据过滤和搜索</h2>
    
    <div class="search-controls">
      <input 
        v-model="searchQuery" 
        placeholder="搜索产品..."
        class="search-input"
      >
      
      <div class="filter-options">
        <label>
          <input type="checkbox" v-model="inStockOnly"> 仅显示有货
        </label>
        
        <select v-model="sortBy" class="sort-select">
          <option value="name">按名称排序</option>
          <option value="priceAsc">价格从低到高</option>
          <option value="priceDesc">价格从高到低</option>
        </select>
      </div>
    </div>
    
    <div class="product-list">
      <div v-if="filteredProducts.length === 0" class="no-results">
        没有找到匹配的产品
      </div>
      
      <div v-for="product in filteredProducts" :key="product.id" class="product-card">
        <h3>{{ product.name }}</h3>
        <p class="price">¥{{ product.price.toFixed(2) }}</p>
        <p class="stock" :class="{ outOfStock: !product.inStock }">
          {{ product.inStock ? '有货' : '缺货' }}
        </p>
      </div>
    </div>
  </div>
</template>

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

// 原始产品数据
const products = ref([
  { id: 1, name: 'Vue3实战教程', price: 89.00, inStock: true },
  { id: 2, name: 'JavaScript高级程序设计', price: 129.00, inStock: true },
  { id: 3, name: 'TypeScript完全指南', price: 79.00, inStock: false },
  { id: 4, name: '前端工程化实践', price: 69.00, inStock: true },
  { id: 5, name: 'React设计模式与最佳实践', price: 99.00, inStock: false },
  { id: 6, name: 'CSS揭秘', price: 59.00, inStock: true }
]);

// 搜索和过滤条件
const searchQuery = ref('');
const inStockOnly = ref(false);
const sortBy = ref('name');

// 过滤和排序产品的计算属性
const filteredProducts = computed(() => {
  // 1. 过滤产品
  let result = [...products.value];
  
  // 搜索过滤
  if (searchQuery.value) {
    const query = searchQuery.value.toLowerCase();
    result = result.filter(product => 
      product.name.toLowerCase().includes(query)
    );
  }
  
  // 库存过滤
  if (inStockOnly.value) {
    result = result.filter(product => product.inStock);
  }
  
  // 2. 排序产品
  switch (sortBy.value) {
    case 'priceAsc':
      result.sort((a, b) => a.price - b.price);
      break;
    case 'priceDesc':
      result.sort((a, b) => b.price - a.price);
      break;
    case 'name':
    default:
      result.sort((a, b) => a.name.localeCompare(b.name));
  }
  
  return result;
});
</script>

<style scoped>
.data-filtering {
  max-width: 1000px;
  margin: 20px auto;
  padding: 20px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
}

.search-controls {
  margin-bottom: 20px;
}

.search-input {
  padding: 8px;
  width: 300px;
  margin-right: 20px;
}

.filter-options {
  display: inline-block;
  margin-left: 20px;
}

.filter-options label {
  margin-right: 20px;
}

.sort-select {
  padding: 6px;
}

.product-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 20px;
}

.product-card {
  padding: 15px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
}

.price {
  color: #e53935;
  font-weight: bold;
}

.stock {
  color: #43a047;
}

.outOfStock {
  color: #f44336;
}

.no-results {
  grid-column: 1 / -1;
  text-align: center;
  padding: 40px;
  color: #757575;
}
</style>

运行结果

  • 在搜索框输入"vue",只显示包含"vue"的产品
  • 勾选"仅显示有货",只显示有库存的产品
  • 选择不同的排序选项,产品会按相应方式排序

代码解析

  • 使用一个计算属性整合过滤和排序逻辑
  • 计算属性依赖于搜索关键词、库存过滤条件和排序选项
  • 当任何过滤条件变化时,计算属性会重新计算并返回过滤后的结果
  • 这种方式使模板非常简洁,所有逻辑集中在计算属性中

3.3 表单验证

计算属性可以用于实时表单验证,提供即时反馈。

<template>
  <div class="form-validation">
    <h2>表单验证</h2>
    
    <form @submit.prevent="handleSubmit">
      <div class="form-group">
        <label>用户名:</label>
        <input 
          v-model="username" 
          placeholder="请输入用户名"
          :class="{ invalid: !usernameValid }"
        >
        <span v-if="!usernameValid" class="error-message">{{ usernameError }}</span>
      </div>
      
      <div class="form-group">
        <label>邮箱:</label>
        <input 
          v-model="email" 
          type="email" 
          placeholder="请输入邮箱"
          :class="{ invalid: !emailValid }"
        >
        <span v-if="!emailValid" class="error-message">{{ emailError }}</span>
      </div>
      
      <div class="form-group">
        <label>密码:</label>
        <input 
          v-model="password" 
          type="password" 
          placeholder="请输入密码"
          :class="{ invalid: !passwordValid }"
        >
        <span v-if="!passwordValid" class="error-message">{{ passwordError }}</span>
      </div>
      
      <div class="form-group">
        <label>确认密码:</label>
        <input 
          v-model="confirmPassword" 
          type="password" 
          placeholder="请确认密码"
          :class="{ invalid: !confirmPasswordValid }"
        >
        <span v-if="!confirmPasswordValid" class="error-message">{{ confirmPasswordError }}</span>
      </div>
      
      <button type="submit" :disabled="!formValid">提交</button>
    </form>
  </div>
</template>

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

// 表单数据
const username = ref('');
const email = ref('');
const password = ref('');
const confirmPassword = ref('');

// 用户名验证
const usernameValid = computed(() => {
  return username.value.length >= 3 && username.value.length <= 20;
});

const usernameError = computed(() => {
  if (username.value.length === 0) return '';
  if (username.value.length < 3) return '用户名至少3个字符';
  if (username.value.length > 20) return '用户名最多20个字符';
  return '';
});

// 邮箱验证
const emailValid = computed(() => {
  if (!email.value) return true; // 空值暂时视为有效,提交时再强制检查
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email.value);
});

const emailError = computed(() => {
  if (!email.value) return '';
  return emailValid.value ? '' : '请输入有效的邮箱地址';
});

// 密码验证
const passwordValid = computed(() => {
  if (!password.value) return true; // 空值暂时视为有效
  return password.value.length >= 6 && /[A-Z]/.test(password.value) && /[0-9]/.test(password.value);
});

const passwordError = computed(() => {
  if (!password.value) return '';
  if (password.value.length < 6) return '密码至少6个字符';
  if (!/[A-Z]/.test(password.value)) return '密码必须包含大写字母';
  if (!/[0-9]/.test(password.value)) return '密码必须包含数字';
  return '';
});

// 确认密码验证
const confirmPasswordValid = computed(() => {
  return password.value ===
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐