Vue3 计算属性与监听器完全指南
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> —— 跨组件深层传递响应式数据的最佳方式