第1章 1.3 计算属性与侦听器
📖 这是系列教程「Vue3 从入门到精通」的第 3 章,上一章我们学了模板语法与指令,这一章要解决一个实际问题。
🎯 开场:为什么需要计算属性?
想象这样一个场景:小明在电商公司做运营,每天要对着一份 Excel 表格发呆。
表格里有几百行商品数据,他要找出:
- 所有「价格大于 100 且库存小于 50」的商品
- 这些商品的总销售额是多少
- 库存告急(<20)的商品有哪些
你会怎么做?
# 普通人做法:直接在模板里写逻辑
{{ products.filter(p => p.price > 100 && p.stock < 50).reduce((sum, p) => sum + p.price * p.stock, 0) }}
看着就头皮发麻对吧?更糟糕的是——如果这份数据要重复用,你得复制粘贴 N 遍,改一处要改 N 个地方。
学完这章,你就能:用计算属性和侦听器,把复杂逻辑封装起来,像搭积木一样构建你的页面。
🧱 基础:\n\n
\n\n
\n\n核心概念
什么是计算属性?
生活类比:计算属性就像餐厅的「自动计算小票机」。
- 你点了几道菜,小票机自动帮你加总价格
- 你删了一道菜,小票机自动重新算一遍
- 你不需要每次都说「帮我算一下总价」,它自己会算
代码栗子:
<script setup>
import { ref, computed } from 'vue'
const products = ref([
{ name: 'iPhone', price: 6999, stock: 45 },
{ name: 'AirPods', price: 999, stock: 120 },
{ name: 'MacBook', price: 9999, stock: 15 },
])
// 计算属性:一句话搞定筛选+统计
const expensiveLowStockItems = computed(() => {
return products.value
.filter(p => p.price > 1000 && p.stock < 50)
})
</script>
computed 就是那个自动计算小票机,你只需要定义「算什么」,它自己会「算对」。
什么是侦听器?
生活类比:侦听器就像「快递物流提醒」。
- 你下单后,系统会盯着物流状态
- 一旦状态变了(「已发货」「运输中」「派送中」),立刻通知你
- 你不用每秒刷新一次查物流,它变了自己会告诉你
代码栗子:
<script setup>
import { ref, watch } from 'vue'
const orderStatus = ref('已下单')
const notification = ref('')
// 侦听器:盯着 orderStatus,一旦变了就执行回调
watch(orderStatus, (newStatus, oldStatus) => {
notification.value = `订单从「${oldStatus}」变成了「${newStatus}」`
})
// 模拟状态变化
setTimeout(() => orderStatus.value = '已发货', 2000)
</script>
watch 就是那个物流提醒,你定义「盯什么」和「变了怎么办」。
computed vs methods:到底用哪个?
很多人分不清 computed 和 methods 的区别,说白了就是:
| 计算属性 | 方法 | |
|---|---|---|
| 调用方式 | 当属性用 {{ total }} |
当函数用 {{ getTotal() }} |
| 缓存 | ✅ 有缓存,依赖不变不重算 | ❌ 每次都重新执行 |
| 适用场景 | 派生状态(从原数据计算出来的) | 纯操作(做一些动作) |
一个例子看区别:
<script setup>
import { ref, computed, methods } from 'vue'
const count = ref(0)
// 计算属性:有缓存
const doubled = computed(() => {
console.log('计算属性执行了')
return count.value * 2
})
// 方法:没缓存
const getDoubled = () => {
console.log('方法执行了')
return count.value * 2
}
</script>
<template>
<p>计算属性:{{ doubled }}</p> <!-- 第一次执行,后续依赖不变不重算 -->
<p>方法:{{ getDoubled() }}</p> <!-- 每次都执行 -->
</template>
watchEffect:更偷懒的侦听方式
有时候你不想写「监听谁」,只想写「依赖啥」。
生活类比:就像你点外卖后,只关心「外卖到了吗」,不关心物流具体走到哪了。
<script setup>
import { ref, watchEffect } from 'vue'
const name = ref('小明')
const age = ref(18)
// 自动监听函数内用到的所有响应式数据
watchEffect(() => {
console.log(`用户信息改变了:${name.value},${age.value}岁`)
})
</script>
比 watch 简洁,但缺点是:不知道是谁触发的。
🔥 实战:3 个小项目
项目 1:个人开销统计(5 分钟)
目标:输入一周开销,自动统计总支出和平均日支出。
<script setup>
import { ref, computed } from 'vue'
const expenses = ref([
{ day: '周一', amount: 45 },
{ day: '周二', amount: 120 },
{ day: '周三', amount: 30 },
{ day: '周四', amount: 80 },
{ day: '周五', amount: 200 },
{ day: '周六', amount: 350 },
{ day: '周日', amount: 90 },
])
// 计算属性自动算好,用的时候直接拿
const totalExpense = computed(() => {
return expenses.value.reduce((sum, e) => sum + e.amount, 0)
})
const avgDaily = computed(() => {
return (totalExpense.value / expenses.value.length).toFixed(2)
})
</script>
<template>
<h3>本周开销清单</h3>
<ul>
<li v-for="e in expenses" :key="e.day">
{{ e.day }}:¥{{ e.amount }}
</li>
</ul>
<p>总支出:<strong>¥{{ totalExpense }}</strong></p>
<p>日均:<strong>¥{{ avgDaily }}</strong></p>
</template>
预期输出:
本周开销清单
- 周一:¥45
- 周二:¥120
- ...
总支出:¥915
日均:¥130.71
跟着抄一遍,是不是感觉计算属性超好用?
项目 2:商品搜索过滤(15 分钟)
目标:从 JSON 数据中搜索商品,支持按价格区间筛选。
<script setup>
import { ref, computed } from 'vue'
const products = ref([
{ id: 1, name: 'iPhone 15', price: 6999, stock: 45, category: '手机' },
{ id: 2, name: '小米手环', price: 299, stock: 200, category: '穿戴' },
{ id: 3, name: 'AirPods Pro', price: 1899, stock: 80, category: '配件' },
{ id: 4, name: 'iPad Air', price: 4799, stock: 30, category: '平板' },
{ id: 5, name: 'Apple Watch', price: 2999, stock: 60, category: '穿戴' },
])
const searchKeyword = ref('')
const minPrice = ref(0)
const maxPrice = ref(99999)
// 搜索条件变了,过滤结果自动更新
const filteredProducts = computed(() => {
return products.value.filter(p => {
const matchName = p.name.includes(searchKeyword.value)
const matchPrice = p.price >= minPrice.value && p.price <= maxPrice.value
return matchName && matchPrice
})
})
const resultCount = computed(() => filteredProducts.value.length)
</script>
<template>
<h3>商品搜索</h3>
<input v-model="searchKeyword" placeholder="搜索商品名称" />
<input v-model.number="minPrice" type="number" placeholder="最低价" />
<input v-model.number="maxPrice" type="number" placeholder="最高价" />
<p>找到 {{ resultCount }} 个商品:</p>
<ul>
<li v-for="p in filteredProducts" :key="p.id">
{{ p.name }} - ¥{{ p.price }} - 库存{{ p.stock }}
</li>
</ul>
</template>
关键点:计算属性 filteredProducts 自动监听 searchKeyword、minPrice、maxPrice 的变化,你不需要手动刷新。
项目 3:待办清单 with 状态持久化(15 分钟)
目标:做一个带分类筛选的待办清单,数据存 localStorage,刷新不丢失。
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
// 从 localStorage 读取历史数据
const todos = ref(JSON.parse(localStorage.getItem('todos') || '[]'))
const newTodo = ref('')
const filter = ref('all') // all / active / done
// 计算属性:过滤后的待办列表
const filteredTodos = computed(() => {
if (filter.value === 'active') return todos.value.filter(t => !t.done)
if (filter.value === 'done') return todos.value.filter(t => t.done)
return todos.value
})
const activeCount = computed(() => todos.value.filter(t => !t.done).length)
const doneCount = computed(() => todos.value.filter(t => t.done).length)
// 侦听器:todos 变了自动存 localStorage
watch(todos, (newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })
// 添加待办
const addTodo = () => {
if (!newTodo.value.trim()) return
todos.value.push({
id: Date.now(),
text: newTodo.value,
done: false,
createdAt: new Date().toLocaleDateString()
})
newTodo.value = ''
}
// 切换完成状态
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.done = !todo.done
}
// 删除待办
const deleteTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>
<template>
<h2>待办清单</h2>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="输入待办事项..." />
<button @click="addTodo">添加</button>
<div class="filters">
<button :class="{ active: filter === 'all' }" @click="filter = 'all'">全部</button>
<button :class="{ active: filter === 'active' }" @click="filter = 'active'">进行中</button>
<button :class="{ active: filter === 'done' }" @click="filter = 'done'">已完成</button>
</div>
<ul>
<li v-for="todo in filteredTodos" :key="todo.id" :class="{ done: todo.done }">
<input type="checkbox" :checked="todo.done" @change="toggleTodo(todo.id)" />
<span>{{ todo.text }}</span>
<small>{{ todo.createdAt }}</small>
<button @click="deleteTodo(todo.id)">删除</button>
</li>
</ul>
<p>进行中:{{ activeCount }} | 已完成:{{ doneCount }}</p>
</template>
<style>
.done span { text-decoration: line-through; color: #999; }
.active { background: #4CAF50; color: white; }
</style>
核心能力回顾:
- computed:自动计算「进行中」「已完成」数量
- watch:自动把数据持久化到 localStorage
- 组合使用,代码简洁又健壮
💪 进阶:常见坑 + 技巧
坑 1:计算属性不是函数
<!-- ❌ 错误:把计算属性当函数调用 -->
<p>{{ totalExpense() }}</p>
<!-- ✅ 正确:计算属性是响应式属性,不用加 () -->
<p>{{ totalExpense }}</p>
坑 2:watch 监听不到对象内部变化
<script setup>
// ❌ 这样写监听不到 user.name 的变化
watch(user, (newUser) => console.log(newUser))
// ✅ 正确写法 1:深度监听
watch(user, (newUser) => console.log(newUser), { deep: true })
// ✅ 正确写法 2:监听具体属性
watch(() => user.value.name, (newName) => console.log(newName))
</script>
坑 3:computed 和 watch 用混了
<script setup>
// ❌ 用 watch 做计算属性该做的事(冗余)
const count = ref(0)
watch(count, (newCount) => {
doubled.value = newCount * 2 // 还要手动维护 doubled
})
// ✅ 用 computed 做计算属性该做的事(自动)
const doubled = computed(() => count.value * 2)
</script>
坑 4:异步操作不能用 computed
<script setup>
// ❌ computed 不能写异步
const data = computed(async () => {
const res = await fetch('/api/data')
return res.json()
})
// ✅ 异步操作用 watchEffect 或在 onMounted 里处理
import { watchEffect } from 'vue'
watchEffect(async () => {
const res = await fetch('/api/data')
data.value = await res.json()
})
</script>
调试技巧:给计算属性加 console.log
<script setup>
const result = computed(() => {
console.log('计算属性执行了,当前值:', complicatedLogic())
return complicatedLogic()
})
</script>
✏️ 练习题
练习 1(2 分钟):基础抄改
- 输入:products = [{price: 100}, {price: 200}]
- 预期输出:total = 300
- 提示:把项目 1 的计算属性改一下数据源
练习 2(2 分钟):加个条件
- 在练习 1 基础上,加一个筛选条件:只统计价格 > 150 的商品
- 预期输出:total = 200
- 提示:在 filter 里加个条件
练习 3(3 分钟):换个数据源
- 用这个数组:[{name: 'A', score: 85}, {name: 'B', score: 92}]
- 用计算属性输出平均分
- 提示:项目 1 的 avgDaily 改一下就行
练习 4(5 分钟):串起来
- 把练习 2 和练习 3 合并成一个组件
- 同时显示:筛选后的总数、平均分
- 提示:一个 computed 统计数量,一个计算平均分
练习 5(5 分钟):报错分析
- 输入:以下代码报错 "Cannot read property 'value' of undefined"
const result = computed(() => data.value.filter(d => d > 10))
- 预期输出:说出原因并修复
- 提示:检查 data 是否在 setup 里正确定义
作业:做一个「个人资产负债表」
- 需求:录入收入和支出,自动计算净资产
- 功能点:
1. 添加收入/支出条目(类型、金额、备注)
2. 自动计算总收入、总支出、净资产
3. 支持按类型筛选显示
4. 数据存 localStorage(加分项) - 验收标准:能跑起来 + 增删查算都正常 + 代码有注释
- 提交:评论区贴代码截图
📚 总结
这一章学了 3 个核心点:
1. 计算属性:自动缓存的派生状态,用它代替模板里的复杂逻辑
2. 侦听器 watch:盯着某个数据,变了就执行回调(适合副作用)
3. watchEffect:更偷懒的侦听方式,自动收集依赖
推荐资源:
- Vue3 官方文档 - 计算属性
- Vue3 官方文档 - 侦听器
- 《Vue.js 设计与实现》第 4 章(进阶必读)
互动钩子:你在项目里用过计算属性或侦听器吗?遇到过什么坑?评论区聊聊,老粉优先回复!
📌 下一章我们要学「组件基础与 props」,学完你就能把页面拆成可复用的小积木了。

评论(0)