计算属性与侦听器
计算属性与侦听器
- computed:从现有数据“算”出新数据(有缓存,无副作用)
- watch:监听数据变化,执行副作用(如请求、日志、操作上报)
1. 计算属性 computed
为什么需要computed?
模板中直接写复杂表达式会导致臃肿、重复、不可复用。
把“根据现有数据算出新数据”的逻辑放在<script setup>中,模板只负责渲染。
基本用法
import{ref,computed}from'vue'constkeyword=ref('')constusers=ref([...])// 源数据constfilteredUsers=computed(()=>{returnusers.value.filter(user=>user.name.includes(keyword.value))})三个要点
- 模板中直接使用,不要加
.value(自动解包):v-for="user in filteredUsers" - 脚本中必须用
.value读取值:filteredUsers.value.length - 只读:不能给计算属性赋值,要改变结果就修改它依赖的源数据
缓存特性
- computed 会缓存计算结果,只有当依赖的响应式数据变化时才重新计算
- 多次使用同一个计算属性,只会执行一次计算(后续读缓存)
对比普通方法:
- 方法每次被调用都会重新执行(无论依赖是否变化)
- 模板渲染时多次调用方法就会执行多次,无缓存
类比:computed 像电子价目牌(商品不变就不动),方法像收银员每接一个任务就重新计算
误区:计算函数里如果使用了非响应式数据(如Date.now()),Vue 无法感知变化,缓存不会失效。
典型应用:搜索 + 筛选
同时根据关键字和状态过滤列表,两个条件用 AND 叠加:
constkeyword=ref('')conststatusFilter=ref('全部')constfilteredUsers=computed(()=>{returnusers.value.filter(user=>{constmatchKeyword=keyword.value===''||user.name.includes(keyword.value)||user.dept.includes(keyword.value)constmatchStatus=statusFilter.value==='全部'||user.status===statusFilter.valuereturnmatchKeyword&&matchStatus})})模板中直接遍历filteredUsers,人数统计直接用filteredUsers.length。
2. 侦听器 watch
与 computed 的区别
- computed 是纯计算,不应包含副作用(请求、日志等)
- watch 专门用于数据变化时执行副作用
口诀:能算就用 computed,要动才用 watch
基本用法
import{ref,watch}from'vue'constkeyword=ref('')watch(keyword,(newVal,oldVal)=>{console.log(`关键词从「${oldVal}」变为「${newVal}」`)// 可以在这里:发请求、记日志、保存数据等})回调参数:(新值, 旧值)
监听多个数据源
watch([keyword,statusFilter],([newKeyword,newStatus],[oldKeyword,oldStatus])=>{// 任一数据变化都触发})防抖处理(debounce)
连续触发时,只执行最后一次(如输入暂停 500ms 后再发起请求):
lettimer=nullwatch(keyword,(newVal)=>{clearTimeout(timer)timer=setTimeout(()=>{// 执行上报或请求},500)})清除上一次的定时器,保证只有停止输入超过指定时间才会执行。
选项:immediate(立即执行)
- 默认 watch 只在数据第一次变化后才触发
- 设置
immediate: true可在页面初始化时立即执行一次回调(此时oldValue为undefined)
watch(statusFilter,(newVal)=>{// 初始化时也会执行},{immediate:true})常用于:进入页面立即加载数据、记录初始状态等。
选项:deep(深度监听)
- 普通 watch 只监听引用类型变量的引用变化(如数组整体替换)
- 如果需要监听对象内部属性或数组元素变化,设置
deep: true
watch(users,()=>{// 当 users 数组内某个对象的属性被修改时触发},{deep:true})注意事项:
deep有性能损耗,大对象或深层嵌套对象要小心使用- 深度监听时回调中的新旧值是同一个引用,无法获得变化前的快照(
newVal === oldVal) - 如果需要获取旧值,自己在外层保存快照
监听 reactive 对象的某个属性(getter 形式)
import{reactive,watch}from'vue'conststate=reactive({name:'张三',age:30})watch(()=>state.name,(newVal,oldVal)=>{// 只监听 name 属性,无需 deep})这种写法更精确、性能更好。
3. computed vs watch 选型总结
| computed | watch | |
|---|---|---|
| 用途 | 派生数据(计算新值) | 执行副作用(动作) |
| 返回值 | 有返回值,可直接在模板使用 | 无返回值,只执行回调 |
| 缓存 | 有(依赖不变不重复计算) | 无(每次变化都触发) |
| 触发条件 | 依赖变化时自动重新计算 | 被监听数据变化时执行回调 |
| 典型场景 | 搜索过滤、列表统计、格式化显示 | 数据变化请求接口、日志上报、防抖搜索、配置保存 |
| 依赖多个数据 | 多个响应式数据都可作为依赖 | 可监听单个或多个数据源 |
| 异步支持 | 不能包含副作用 | 适合放异步请求 |
选择原则:
- 需要得到一个新数据→ computed
- 需要在数据变化时做一件事→ watch
两者常配合使用,并非二选一。
4. 关键代码速查
搜索筛选(computed)
constfilteredUsers=computed(()=>{returnusers.value.filter(user=>{constmatchKeyword=keyword.value===''||user.name.includes(keyword.value)||user.dept.includes(keyword.value)constmatchStatus=statusFilter.value==='全部'||user.status===statusFilter.valuereturnmatchKeyword&&matchStatus})})联动上报(watch + 防抖)
lettimer=nullwatch([keyword,statusFilter],([newKeyword,newStatus])=>{clearTimeout(timer)timer=setTimeout(()=>{addLog(`筛选条件已变化:关键字「${newKeyword}」、状态「${newStatus}」`)},500)})immediate + deep 示例
// 页面打开立即记录当前筛选状态watch(statusFilter,(newStatus)=>{addLog(`当前筛选状态为「${newStatus}」`)},{immediate:true})// 监听用户列表内部变化watch(users,()=>{addLog('用户列表数据发生变化')},{deep:true})5. 常见错误排查
| 现象 | 原因 | 解决 |
|---|---|---|
模板中filteredUsers.value报错 | 模板自动解包,不需要.value | 直接写filteredUsers |
| 搜索无效,列表不变 | v-for仍遍历源数据users | 改为filteredUsers |
| 给计算属性赋值报错 | computed 只读 | 修改依赖的源数据 |
| 点击“切换状态”日志不触发 | 没有加deep: true | 监听对象/数组内部变化需加deep |
| 每输入一个字符都上报 | 缺少防抖 | 用clearTimeout/setTimeout控制 |
| 刷新页面没有初始化日志 | 忘记immediate: true | 在 watch 配置中添加 |
| deep 监听时新、旧值相同 | 深度监听的新旧值是同引用 | 需要旧值请自行深拷贝保存 |
