04-计算属性与侦听器

📅 2026/6/28 1:50:48
04-计算属性与侦听器
计算属性与侦听器掌握 Vue3 中 computed、watch 和 watchEffect 的用法与差异学会根据场景选择合适的响应式派生与监听方案。一、前言在 Vue 的响应式系统中除了直接定义响应式数据外我们经常需要基于已有数据计算派生值或者在数据变化时执行副作用。Vue3 提供了computed、watch和watchEffect三个核心 API 来满足这些需求。本章将深入讲解它们的用法、差异和最佳实践。二、computed计算属性2.1 基本用法计算属性用于基于响应式数据派生出一个新的值具有缓存特性——只有当依赖的数据变化时才会重新计算。script setup import { ref, computed } from vue const firstName ref(张) const lastName ref(三) // 只读计算属性 const fullName computed(() { return firstName.value lastName.value }) console.log(fullName.value) // 张三 /script template div p姓{{ firstName }}/p p名{{ lastName }}/p p全名{{ fullName }}/p /div /template2.2 可写计算属性通过提供get和set方法可以创建可读写的计算属性。script setup import { ref, computed } from vue const firstName ref(张) const lastName ref(三) // 可写计算属性 const fullName computed({ get() { return firstName.value · lastName.value }, set(newValue) { // 根据新值拆分并更新原始数据 const parts newValue.split(·) firstName.value parts[0] || lastName.value parts[1] || } }) function changeName() { fullName.value 李·四 // 触发 setter } /script template div p全名{{ fullName }}/p input v-modelfullName placeholder输入全名 / button clickchangeName改为李四/button /div /template2.3 computed 的缓存特性计算属性的核心优势在于缓存。与方法不同计算属性只在依赖变化时重新求值。import{ref,computed}fromvueconstcountref(0)// 计算属性有缓存constdoubleCountcomputed((){console.log(计算属性执行)returncount.value*2})// 方法无缓存functiongetDoubleCount(){console.log(方法执行)returncount.value*2}// 多次访问计算属性console.log(doubleCount.value)// 执行计算console.log(doubleCount.value)// 直接返回缓存不执行console.log(doubleCount.value)// 直接返回缓存// 多次调用方法getDoubleCount()// 执行getDoubleCount()// 执行getDoubleCount()// 执行特性computed方法缓存有依赖不变不重新计算无每次调用都执行适用场景基于数据派生值需要参数或执行副作用性能更优避免重复计算每次调用都有开销三、watch侦听器3.1 基本用法watch用于监听响应式数据的变化并在变化时执行回调函数。script setup import { ref, watch } from vue const count ref(0) const message ref() // 监听单个 ref watch(count, (newValue, oldValue) { console.log(count 从 ${oldValue} 变为 ${newValue}) message.value 当前计数${newValue} }) function increment() { count.value } /script template div p{{ message }}/p p计数{{ count }}/p button clickincrement1/button /div /template3.2 监听 reactive 对象的属性script setup import { reactive, watch, toRef } from vue const state reactive({ count: 0, user: { name: 张三, age: 20 } }) // 方式1监听 reactive 对象的属性返回 getter 函数 watch( () state.count, (newVal, oldVal) { console.log(count变化, oldVal, -, newVal) } ) // 方式2使用 toRef const countRef toRef(state, count) watch(countRef, (newVal, oldVal) { console.log(count变化, oldVal, -, newVal) }) // 方式3直接监听整个 reactive 对象深度监听 watch(state, (newVal, oldVal) { // 注意监听整个对象时newVal 和 oldVal 是同一个引用 console.log(state变化) }, { deep: true }) /script3.3 监听多个数据源import{ref,watch}fromvueconstfirstNameref(张)constlastNameref(三)// 同时监听多个数据源watch([firstName,lastName],([newFirst,newLast],[oldFirst,oldLast]){console.log(姓名变化${oldFirst}${oldLast}-${newFirst}${newLast})})3.4 深层监听import{ref,watch}fromvueconstuserref({profile:{name:张三,address:{city:北京}}})// 深层监听对象内部变化watch(user,(newVal,oldVal){console.log(user变化)},{deep:true}// 开启深度监听)// 修改深层属性也会触发user.value.profile.address.city上海注意深层监听会递归遍历对象的所有属性性能开销较大。建议精确监听具体路径。3.5 立即执行import{ref,watch}fromvueconstcountref(0)// 立即执行选项watch(count,(newVal,oldVal){console.log(执行,newVal,oldVal)},{immediate:true}// 初始化时立即执行一次回调)// 输出// 执行 0 undefined 初始化时立即执行// 执行 1 0 count 变化时3.6 回调执行时机flush 选项import{ref,watch,nextTick}fromvueconstcountref(0)// flush 选项控制回调执行时机watch(count,(newVal){console.log(回调执行)console.log(DOM已更新,document.getElementById(count).textContent)},{flush:pre// 默认DOM更新前执行// flush: post // DOM更新后执行// flush: sync // 同步执行谨慎使用})flush 值执行时机使用场景preDOM 更新前默认需要在 DOM 更新前访问旧状态postDOM 更新后需要在回调中访问更新后的 DOMsync同步立即执行需要立即响应变化谨慎使用可能影响性能四、watchEffect自动追踪依赖4.1 基本用法watchEffect会立即执行传入的函数并自动追踪其中使用的响应式数据作为依赖。script setup import { ref, watchEffect } from vue const count ref(0) const name ref(张三) // 自动追踪所有依赖 watchEffect(() { console.log(watchEffect 执行) console.log(count:, count.value) console.log(name:, name.value) }) // 修改任一依赖都会触发 function updateCount() { count.value } function updateName() { name.value 李四 } /script输出顺序// 初始化时立即执行 watchEffect 执行 count: 0 name: 张三 // 修改 count 后 watchEffect 执行 count: 1 name: 张三 // 修改 name 后 watchEffect 执行 count: 1 name: 李四4.2 watchEffect 的特点自动追踪无需显式指定依赖自动收集函数中使用的响应式数据立即执行初始化时立即执行一次无旧值回调不接收旧值参数4.3 停止侦听watch和watchEffect返回一个停止函数可以在必要时停止侦听。import{ref,watch,watchEffect}fromvueconstcountref(0)// watch 返回停止函数conststopWatchwatch(count,(newVal){console.log(watch:,newVal)})// watchEffect 返回停止函数conststopEffectwatchEffect((){console.log(watchEffect:,count.value)})// 停止侦听stopWatch()stopEffect()// 此后修改 count 不会触发回调count.value4.4 副作用清理当watchEffect重新执行时可能需要清理上一次的副作用如取消请求、移除事件监听等。import{ref,watchEffect}fromvueconstuserIdref(1)watchEffect((onCleanup){constcontrollernewAbortController()// 发起请求fetch(/api/user/${userId.value},{signal:controller.signal}).then(resres.json()).then(data{console.log(用户数据,data)})// 注册清理函数onCleanup((){controller.abort()// 取消上一次的请求console.log(清理副作用取消请求)})})// 当 userId 变化时会先执行 onCleanup再执行新的 effectuserId.value2五、watch vs watchEffect 对比特性watchwatchEffect依赖声明显式指定第一个参数自动追踪函数中使用的响应式数据初始执行默认不执行除非设置immediate: true立即执行旧值访问可以获取oldValue无法获取旧值使用场景需要精确控制监听目标、需要旧值需要自动追踪多个依赖、执行副作用深层监听通过deep选项控制自动深度追踪是否是否需要监听响应式数据是否需要精确控制依赖?使用 watch是否需要旧值?使用 watchEffect显式指定依赖自动追踪依赖按需配置 deep/immediate/flush初始化立即执行六、Vue2 vs Vue3 侦听器变化6.1 API 变化特性Vue2Vue3Options APIwatch: { count(newVal, oldVal) {} }相同Composition API无watch(source, callback, options)自动追踪副作用无watchEffect(fn)深层监听deep: truedeep: true立即执行immediate: trueimmediate: true回调时机无控制flush: pre/post/sync6.2 迁移示例Vue2 Options APIexportdefault{data(){return{count:0,message:}},watch:{count(newVal,oldVal){this.message计数变化${oldVal}-${newVal}}}}Vue3 Composition APIscript setup import { ref, watch } from vue const count ref(0) const message ref() watch(count, (newVal, oldVal) { message.value 计数变化${oldVal} - ${newVal} }) /script七、综合示例下面是一个综合使用computed、watch和watchEffect的购物车示例script setup import { ref, computed, watch, watchEffect } from vue // 商品列表 const products ref([ { id: 1, name: 苹果, price: 5, quantity: 2 }, { id: 2, name: 香蕉, price: 3, quantity: 3 }, { id: 3, name: 橙子, price: 4, quantity: 1 } ]) // 计算属性商品总数 const totalItems computed(() { return products.value.reduce((sum, p) sum p.quantity, 0) }) // 计算属性商品总价可写 const totalPrice computed({ get() { return products.value.reduce((sum, p) sum p.price * p.quantity, 0) }, set(newValue) { // 平均分配折扣到每个商品示例逻辑 const ratio newValue / totalPrice.value products.value.forEach(p { p.price Math.round(p.price * ratio * 100) / 100 }) } }) // 计算属性是否有商品 const hasItems computed(() totalItems.value 0) // 侦听器监听总价变化记录日志 watch(totalPrice, (newVal, oldVal) { console.log(总价变化${oldVal}元 - ${newVal}元) }) // 侦听器监听商品数量变化 watch( () products.value.map(p p.quantity), (newQuantities, oldQuantities) { console.log(商品数量变化, newQuantities) } ) // watchEffect自动追踪所有依赖更新页面标题 watchEffect(() { if (hasItems.value) { document.title 购物车(${totalItems.value}) - 共${totalPrice.value}元 } else { document.title 购物车 - 空 } }) // 方法 function increaseQuantity(product) { product.quantity } function decreaseQuantity(product) { if (product.quantity 0) { product.quantity-- } } function removeProduct(id) { const index products.value.findIndex(p p.id id) if (index -1) { products.value.splice(index, 1) } } /script template div classcart h2购物车/h2 div v-if!hasItems classempty 购物车是空的 /div ul v-else classproduct-list li v-forproduct in products :keyproduct.id span classname{{ product.name }}/span span classprice{{ product.price }}元/个/span div classquantity-control button clickdecreaseQuantity(product)-/button span{{ product.quantity }}/span button clickincreaseQuantity(product)/button /div span classsubtotal小计{{ product.price * product.quantity }}元/span button classremove clickremoveProduct(product.id)删除/button /li /ul div classsummary v-ifhasItems p商品总数{{ totalItems }} 件/p p商品总价{{ totalPrice }} 元/p /div /div /template style scoped .cart { max-width: 600px; margin: 40px auto; padding: 20px; font-family: Arial, sans-serif; } .product-list { list-style: none; padding: 0; } .product-list li { display: flex; align-items: center; gap: 15px; padding: 15px; border-bottom: 1px solid #eee; } .name { font-weight: bold; width: 80px; } .price { color: #666; width: 80px; } .quantity-control { display: flex; align-items: center; gap: 10px; } .quantity-control button { width: 30px; height: 30px; border: 1px solid #ddd; background: white; cursor: pointer; border-radius: 4px; } .subtotal { color: #42b983; font-weight: bold; flex: 1; } .remove { background: #ff6b6b; color: white; border: none; padding: 5px 15px; border-radius: 4px; cursor: pointer; } .summary { margin-top: 20px; padding-top: 20px; border-top: 2px solid #42b983; font-size: 18px; } .empty { text-align: center; color: #999; padding: 40px; } /style八、常见问题Q1computed 和 watch 有什么区别维度computedwatch目的派生新值执行副作用返回值有派生值无缓存有无使用场景模板中展示派生数据数据变化时执行异步或复杂逻辑原则需要派生值用computed需要执行副作用用watch/watchEffect。Q2为什么 watch 监听 reactive 对象时 oldValue 和 newValue 相同因为reactive返回的是代理对象Vue 不会保留其浅拷贝。如果需要获取旧值建议监听对象的某个具体属性返回 getter 函数或者使用ref。// 这样 oldValue 和 newValue 是同一个引用watch(state,(newVal,oldVal){console.log(newValoldVal)// true})// 正确做法监听具体属性watch(()state.count,(newVal,oldVal){console.log(newVal,oldVal)// 正常获取旧值})Q3watchEffect 中的依赖是如何追踪的Vue 在watchEffect的回调函数执行期间会自动追踪所有被访问的响应式数据ref的.value、reactive的属性。这些数据会被记录为依赖当任何依赖变化时回调会重新执行。watchEffect((){// 以下被访问的数据都会被追踪为依赖console.log(count.value)// 追踪 countconsole.log(state.name)// 追踪 state.nameconsole.log(list.value[0])// 追踪 list})Q4什么时候应该使用 flush: ‘post’当你需要在侦听器回调中访问更新后的 DOM 时watch(count,(){// 默认 flush: pre此时 DOM 还未更新console.log(document.getElementById(count).textContent)// 旧值},{flush:post})// 或者使用 nextTickwatch(count,async(){awaitnextTick()console.log(document.getElementById(count).textContent)// 新值})九、总结本章深入讲解了 Vue3 的计算属性与侦听器computed基于响应式数据派生新值具有缓存特性支持只读和可写两种模式watch显式监听指定数据源的变化可获取旧值支持deep、immediate、flush选项watchEffect自动追踪依赖立即执行适合副作用场景支持副作用清理停止侦听watch和watchEffect都返回停止函数可手动停止侦听Vue2 vs Vue3Composition API 提供了更灵活的侦听方式watchEffect是新增的重要 API选择指南需要派生值并缓存 →computed需要精确控制监听目标、获取旧值 →watch需要自动追踪多个依赖、执行副作用 →watchEffect十、练习使用computed实现一个搜索过滤功能根据输入关键词过滤列表数据使用watch监听一个表单对象当任何字段变化时输出变化日志注意处理旧值获取问题使用watchEffect实现一个自动保存功能当用户输入停止 1 秒后自动保存数据提示结合setTimeout和onCleanup对比以下两种写法的差异解释为什么一种会触发更新而另一种不会// 写法Aconststatereactive({count:0})watch(()state.count,(newVal,oldVal)console.log(newVal,oldVal))// 写法Bconstcountref(0)watch(count.value,(newVal,oldVal)console.log(newVal,oldVal))编写一个带有加载状态的异步数据获取组件使用watchEffect处理请求和取消逻辑