Vue3 计算属性与监听器完全指南

小飞兽 Vue.js 216 次阅读 2026-06-19

Vue3 计算属性与监听器完全指南

概述

计算属性(computed)和监听器(watch / watchEffect)是 Vue3 响应式系统中两个核心的派生机制。computed 用于声明式地推导新值,自动缓存依赖,只在变化时重新计算;watch 用于执行有副作用的逻辑,如数据验证、API 调用、外部状态同步。掌握两者的细微差别与配合使用,是 Vue3 中高级组件开发的关键。

基础语法

import { computed, watch, watchEffect, ref, reactive } from 'vue'

computed:接收一个 getter 函数或带有 get/set 的对象,返回一个 ref。

const 派生值 = computed(() => 表达式)                          // 只读
const 读写属性 = computed({
  get: () => 表达式,
  set: (val) => { /* 更新逻辑 */ }
})

watch:显式监听一个或多个响应式数据源,只有变化时才执行回调。

watch(source, (newVal, oldVal) => { /* 副作用 */ })
watch(source, (newVal, oldVal) => {}, { immediate: true, deep: true })

watchEffect:立即执行回调,自动收集回调内所有访问到的响应式依赖。

watchEffect(() => { /* 依赖追踪的副作用 */ })

完整代码示例

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

// ========== 场景:电商商品筛选器 ==========

// 原始数据
const products = ref([
  { id: 1, name: 'iPhone 15', price: 6999, category: '手机', stock: 50, tags: ['旗舰', '5G'] },
  { id: 2, name: 'MacBook Pro', price: 12999, category: '电脑', stock: 20, tags: ['旗舰', '专业'] },
  { id: 3, name: 'AirPods Pro', price: 1899, category: '配件', stock: 100, tags: ['降噪'] },
  { id: 4, name: '小米13', price: 3999, category: '手机', stock: 30, tags: ['性价比', '5G'] },
  { id: 5, name: 'iPad Air', price: 4799, category: '平板', stock: 0, tags: ['轻便'] },
])

const filters = reactive({
  category: '',
  minPrice: 0,
  maxPrice: 20000,
  inStockOnly: false,
  searchKeyword: ''
})

// ========== computed:自动缓存的派生数据 ==========

// 基础筛选:根据分类、价格区间、库存、关键词过滤
const filteredProducts = computed(() => {
  return products.value.filter(p => {
    const matchCategory = !filters.category || p.category === filters.category
    const matchPrice = p.price >= filters.minPrice && p.price <= filters.maxPrice
    const matchStock = !filters.inStockOnly || p.stock > 0
    const matchKeyword = !filters.searchKeyword || 
      p.name.toLowerCase().includes(filters.searchKeyword.toLowerCase()) ||
      p.tags.some(tag => tag.toLowerCase().includes(filters.searchKeyword.toLowerCase()))
    return matchCategory && matchPrice && matchStock && matchKeyword
  })
})

// 派生统计:计算筛选后的商品数量、总价区间、平均价格
const stats = computed(() => {
  const count = filteredProducts.value.length
  if (count === 0) return { count: 0, minPrice: 0, maxPrice: 0, avgPrice: 0 }
  const prices = filteredProducts.value.map(p => p.price)
  return {
    count,
    minPrice: Math.min(...prices),
    maxPrice: Math.max(...prices),
    avgPrice: Math.round(prices.reduce((a, b) => a + b, 0) / count)
  }
})

// 所有分类选项(去重)
const categories = computed(() => {
  return [...new Set(products.value.map(p => p.category))]
})

// 可写的计算属性:批量设置价格区间
const priceRange = computed({
  get: () => [filters.minPrice, filters.maxPrice],
  set: ([min, max]) => {
    filters.minPrice = min
    filters.maxPrice = max
  }
})

// ========== watch:精确控制副作用 ==========

// 监听关键词变化,延迟执行搜索建议(防抖场景模拟)
let suggestTimer = null
watch(() => filters.searchKeyword, (newKeyword) => {
  if (suggestTimer) clearTimeout(suggestTimer)
  if (!newKeyword) return
  suggestTimer = setTimeout(() => {
    console.log(`搜索建议:正在查找 "${newKeyword}" 相关商品...`)
  }, 300)
})

// 监听分类变化,打印分析日志
watch(() => filters.category, (newCat, oldCat) => {
  console.log(`分类从 ${oldCat || '全部'} 切换到 ${newCat || '全部'},结果:${stats.value.count} 件`)
})

// 监听 stats,深度通知外部系统(如百度统计)
watch(stats, (newStats) => {
  // 模拟发送到分析系统
  console.log('[Analytics] 筛选结果变化:', JSON.stringify(newStats))
}, { deep: true })

// ========== watchEffect:自动依赖追踪的副作用 ==========

// 自动收集依赖:回调中访问了哪些响应式数据,哪些数据变化就重新执行
watchEffect(() => {
  const msg = `当前筛选: ${filters.category || '全部'} 类, 
    价格 ${filters.minPrice}-${filters.maxPrice} 元,
    共 ${filteredProducts.value.length} 件商品`
  console.log('[自动追踪]', msg)
  // 该回调会在 filters.category / minPrice / maxPrice / filteredProducts 任一变化时重新执行
})

// ========== watch vs watchEffect 对比演示 ==========

// watch 只追踪明确指定的数据源
watch(filters.inStockOnly, (val) => {
  console.log('[watch] inStockOnly 变为:', val)
})

// watchEffect 自动追踪回调内所有响应式读取
watchEffect(() => {
  // filters.inStockOnly 被读取,所以它变化时这个回调也会执行
  console.log('[watchEffect] inStockOnly:', filters.inStockOnly)
})

// ========== 批量重置筛选条件 ==========
function resetFilters() {
  filters.category = ''
  filters.minPrice = 0
  filters.maxPrice = 20000
  filters.inStockOnly = false
  filters.searchKeyword = ''
}
</script>

<template>
  <div class="product-filter">
    <h2>商品筛选器(计算属性 vs 监听器演示)</h2>

    <!-- 筛选控件 -->
    <div class="filters">
      <input v-model="filters.searchKeyword" placeholder="搜索商品或标签..." />
      <select v-model="filters.category">
        <option value="">全部分类</option>
        <option v-for="cat in categories" :key="cat" :value="cat">{{ cat }}</option>
      </select>
      <label>
        最低价: <input v-model.number="filters.minPrice" type="number" min="0" />
      </label>
      <label>
        最高价: <input v-model.number="filters.maxPrice" type="number" />
      </label>
      <label>
        <input type="checkbox" v-model="filters.inStockOnly" />
        仅显示有货
      </label>
      <button @click="resetFilters">重置</button>
    </div>

    <!-- 统计信息 -->
    <div class="stats">
      <span>共 {{ stats.count }} 件</span>
      <span v-if="stats.count > 0">
        价格区间: ¥{{ stats.minPrice }} - ¥{{ stats.maxPrice }},
        平均 ¥{{ stats.avgPrice }}
      </span>
    </div>

    <!-- 商品列表 -->
    <ul>
      <li v-for="p in filteredProducts" :key="p.id">
        {{ p.name }} - ¥{{ p.price }}
        <span v-if="p.stock === 0" class="sold-out">(缺货)</span>
        <span v-else>(库存 {{ p.stock }})</span>
      </li>
      <li v-if="filteredProducts.length === 0">无匹配商品</li>
    </ul>
  </div>
</template>

<style scoped>
.filters { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.stats { margin: 0.5rem 0; font-size: 0.9rem; color: #666; }
.sold-out { color: red; }
</style>

运行效果说明

页面显示商品列表和多个筛选控件。当用户输入关键词、选择分类、调整价格滑块或切换"仅显示有货"时,filteredProducts 计算属性立即自动重新计算,列表实时更新。stats 派生统计同步刷新。控制台中,watchEffect 在任何筛选条件变化时打印日志,而 watch 精确监听单个条件变化执行特定逻辑(如搜索建议防抖)。

常见问题

Q1:computed 和 methods 有什么区别?

methods 每次调用都会重新执行函数,没有缓存;computed 基于响应式依赖缓存,只有依赖变化才重新计算。例如 filteredProducts 被多个地方使用但商品数据没变化时,不会重复执行过滤逻辑,只返回缓存结果,性能优势明显。

Q2:watch 的 <code>immediate: true</code> 选项什么时候用?

默认情况下 watch 只在数据变化后执行回调,初始化时不执行。加 { immediate: true } 后,回调会在 watch 建立时立即执行一次(oldVal 此时为 undefined)。适合需要立即读取初始值并执行初始化逻辑的场景,如表单初始值校验。

Q3:watchEffect 在 Vue3 中有什么使用禁忌?

watchEffect 的回调应该是同步的,不应该在回调中执行异步操作并期望依赖追踪正常工作。异步操作中的响应式读取不会被追踪。另外,watchEffect 回调中修改被追踪的依赖时,要小心无限循环(修改触发回调,回调再修改),此时应换用 watch 显式控制。

延伸阅读

  • <a href="#">Vue3 组合式 API 完全指南</a> —— 组合式 API 核心概念与项目实践
  • <a href="#">Vue3 响应式系统深度解析:ref vs reactive</a> —— 两种响应式数据创建方式的底层原理
  • <a href="#">Vue3 依赖注入:provide/inject 详解</a> —— 跨组件深层传递响应式数据的最佳方式