面试题:Vue 中 computed 和 watch 的区别是什么?

在 Vue 中,computedwatch 都是用于响应数据变化的特性,但它们在用途和工作方式上有明显的区别。

🎯 核心区别总结

特性computedwatch
用途计算衍生数据监听数据变化执行副作用
返回值必须返回一个值不需要返回值
缓存有缓存,依赖不变不重新计算无缓存,每次变化都执行
异步不支持异步操作支持异步操作
调用时机依赖变化时自动计算监听的数据变化时执行
语法函数形式对象形式,包含 handler、deep 等

💡 computed 详解

基本用法

<template>
  <div>
    <p>原始价格: {{ price }}元</p>
    <p>折扣后价格: {{ discountedPrice }}元</p>
    <p>总价: {{ totalPrice }}元</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      price: 100,
      quantity: 2,
      discount: 0.8
    }
  },
  computed: {
    // 基本的计算属性
    discountedPrice() {
      return this.price * this.discount;
    },

    // 依赖多个数据的计算属性
    totalPrice() {
      return this.discountedPrice * this.quantity;
    },

    // 带 setter 的计算属性
    formattedPrice: {
      get() {
        return `¥${this.price}`;
      },
      set(newValue) {
        // 移除货币符号并转换为数字
        this.price = parseFloat(newValue.replace('¥', ''));
      }
    }
  }
}
</script>

计算属性特点

  1. 缓存机制:依赖数据不变时,直接返回缓存结果
  2. 同步计算:必须是同步操作,不能包含异步逻辑
  3. 声明式:关注”是什么”,而不是”怎么做”

🔍 watch 详解

基本用法

<template>
  <div>
    <input v-model="searchQuery" placeholder="搜索...">
    <p>搜索结果数量: {{ resultCount }}</p>
    <p>搜索历史: {{ searchHistory }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchQuery: '',
      resultCount: 0,
      searchHistory: []
    }
  },
  watch: {
    // 基本监听
    searchQuery(newVal, oldVal) {
      console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`);
      this.performSearch();
    },

    // 立即执行 + 深度监听
    filters: {
      handler(newFilters) {
        console.log('过滤器变化:', newFilters);
        this.refreshData();
      },
      immediate: true, // 立即执行一次
      deep: true       // 深度监听对象内部变化
    }
  },
  methods: {
    performSearch() {
      // 模拟 API 调用
      setTimeout(() => {
        this.resultCount = Math.floor(Math.random() * 100);
        this.searchHistory.push(this.searchQuery);
      }, 300);
    },
    refreshData() {
      console.log('重新加载数据');
    }
  }
}
</script>

监听对象属性

<script>
export default {
  data() {
    return {
      user: {
        name: 'John',
        profile: {
          age: 25,
          email: 'john@example.com'
        }
      }
    }
  },
  watch: {
    // 监听对象特定属性
    'user.name'(newName, oldName) {
      console.log(`用户名从 ${oldName} 改为 ${newName}`);
    },

    // 深度监听整个对象
    user: {
      handler(newUser) {
        console.log('用户信息发生变化:', newUser);
      },
      deep: true
    },

    // 监听嵌套对象属性
    'user.profile.age': {
      handler(newAge) {
        console.log('年龄更新为:', newAge);
      },
      immediate: true
    }
  }
}
</script>

🎨 实际场景对比

场景一:表单验证

<template>
  <div>
    <input v-model="email" placeholder="邮箱">
    <p v-if="emailError" class="error">{{ emailError }}</p>

    <input v-model="phone" placeholder="手机号">
    <p v-if="phoneError" class="error">{{ phoneError }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      email: '',
      phone: '',
      phoneError: ''
    }
  },
  computed: {
    // 使用 computed - 适合即时验证
    emailError() {
      if (!this.email) return '邮箱不能为空';
      if (!this.email.includes('@')) return '邮箱格式不正确';
      return '';
    }
  },
  watch: {
    // 使用 watch - 适合需要异步验证的场景
    phone: {
      handler(newPhone) {
        if (!newPhone) {
          this.phoneError = '手机号不能为空';
          return;
        }

        // 模拟异步验证(如检查手机号是否已注册)
        setTimeout(() => {
          if (newPhone.length !== 11) {
            this.phoneError = '手机号格式不正确';
          } else {
            this.phoneError = '';
          }
        }, 500);
      },
      immediate: true
    }
  }
}
</script>

场景二:数据过滤和搜索

<template>
  <div>
    <input v-model="searchText" placeholder="搜索商品">
    <select v-model="category">
      <option value="">所有分类</option>
      <option value="electronics">电子产品</option>
      <option value="clothing">服装</option>
    </select>

    <div v-for="product in filteredProducts" :key="product.id">
      {{ product.name }} - {{ product.price }}
    </div>

    <p>价格超过1000的商品: {{ expensiveProductsCount }}个</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchText: '',
      category: '',
      products: [
        { id: 1, name: 'iPhone', price: 5999, category: 'electronics' },
        { id: 2, name: 'T-shirt', price: 99, category: 'clothing' },
        { id: 3, name: 'MacBook', price: 12999, category: 'electronics' }
      ]
    }
  },
  computed: {
    // 使用 computed - 数据过滤和衍生
    filteredProducts() {
      return this.products.filter(product => {
        const matchesSearch = product.name.toLowerCase()
          .includes(this.searchText.toLowerCase());
        const matchesCategory = !this.category || 
          product.category === this.category;
        return matchesSearch && matchesCategory;
      });
    },

    expensiveProductsCount() {
      return this.products.filter(product => product.price > 1000).length;
    }
  },
  watch: {
    // 使用 watch - 执行副作用操作
    filteredProducts: {
      handler(newProducts) {
        console.log(`筛选出 ${newProducts.length} 个商品`);
        this.sendAnalytics(newProducts.length);
      },
      immediate: true
    },

    searchText() {
      this.saveSearchHistory();
    }
  },
  methods: {
    sendAnalytics(count) {
      // 发送分析数据
      console.log('发送分析数据:', count);
    },

    saveSearchHistory() {
      // 保存搜索历史
      console.log('保存搜索历史:', this.searchText);
    }
  }
}
</script>

场景三:路由参数监听

<script>
export default {
  data() {
    return {
      userData: null,
      loading: false
    }
  },
  computed: {
    // 从路由参数获取用户ID
    userId() {
      return this.$route.params.userId;
    }
  },
  watch: {
    // 监听用户ID变化,加载用户数据
    userId: {
      handler(newUserId) {
        if (newUserId) {
          this.loadUserData(newUserId);
        }
      },
      immediate: true
    }
  },
  methods: {
    async loadUserData(userId) {
      this.loading = true;
      try {
        // 模拟 API 调用
        const response = await fetch(`/api/users/${userId}`);
        this.userData = await response.json();
      } catch (error) {
        console.error('加载用户数据失败:', error);
      } finally {
        this.loading = false;
      }
    }
  }
}
</script>

🚀 最佳实践指南

使用 computed 的情况:

  • 衍生数据:基于现有数据计算新值
  • 模板中的复杂表达式:简化模板逻辑
  • 需要缓存:避免重复计算
  • 多个数据依赖:一个值依赖多个数据源

使用 watch 的情况:

  • 异步操作:数据变化需要发起 API 请求
  • 副作用:数据变化需要执行其他操作
  • 深度监听:需要监听对象或数组的内部变化
  • 昂贵操作:需要防抖或节流的操作

组合使用示例

<script>
export default {
  data() {
    return {
      items: [],
      filter: '',
      sortBy: 'name'
    }
  },
  computed: {
    // 计算过滤和排序后的数据
    processedItems() {
      let result = this.items.filter(item => 
        item.name.includes(this.filter)
      );

      return result.sort((a, b) => 
        a[this.sortBy].localeCompare(b[this.sortBy])
      );
    }
  },
  watch: {
    // 监听处理后的数据变化,执行副作用
    processedItems: {
      handler(newItems) {
        this.updatePagination();
        this.sendToAnalytics(newItems);
      },
      deep: true
    },

    // 防抖搜索
    filter: {
      handler() {
        clearTimeout(this.debounceTimer);
        this.debounceTimer = setTimeout(() => {
          this.performSearch();
        }, 500);
      }
    }
  },
  methods: {
    updatePagination() {
      // 更新分页信息
    },
    sendToAnalytics(items) {
      // 发送分析数据
    },
    performSearch() {
      // 执行搜索
    }
  }
}
</script>

📝 总结

  • computed:用于声明式的数据衍生,关注”计算结果”
  • watch:用于命令式的响应操作,关注”变化响应”

简单记忆

  • 想要一个新的数据值 → 用 computed
  • 想要在数据变化时执行操作 → 用 watch
  • 需要缓存优化 → 用 computed
  • 需要异步操作 → 用 watch

正确选择使用场景可以让代码更清晰、性能更优化!

THE END
喜欢就支持一下吧
点赞10 分享