03-响应式系统学习目标完成本章学习后你将能够理解Vue 3响应式系统的原理掌握ref和reactive的使用和区别理解响应式数据的解构和传递使用toRef和toRefs掌握computed计算属性理解watch和watchEffect的使用场景前置知识学习本章内容前你需要掌握Vue基础 - Vue的基本语法组件基础 - 组件的定义和使用JavaScript基础 - ES6语法ES6 Proxy - 了解Proxy对象问题引入实际场景在开发一个购物车功能时你会遇到这样的需求// 普通JavaScript对象letcart{items:[],total:0};// 添加商品cart.items.push({id:1,name:iPhone,price:5999});cart.total5999;// 问题页面不会自动更新// 你需要手动操作DOM来更新显示document.getElementById(total).textContentcart.total;每次数据变化都要手动更新DOM非常繁琐且容易出错。为什么需要响应式系统Vue的响应式系统可以自动追踪数据变化并更新视图script setup import { reactive } from vue; // 响应式对象 const cart reactive({ items: [], total: 0 }); // 添加商品 - 页面会自动更新 const addItem (item) { cart.items.push(item); cart.total item.price; }; /script template div p商品数量{{ cart.items.length }}/p p总价¥{{ cart.total }}/p /div /template响应式系统的优势自动更新数据变化时视图自动更新简化代码不需要手动操作DOM性能优化Vue会批量更新避免不必要的渲染易于维护数据和视图的关系清晰明了核心概念概念1ref - 基本类型的响应式API名称ref【作用】创建一个响应式引用对象包装任何类型的值使其具有响应性【使用场景】创建基本类型的响应式数据string、number、boolean等需要重新赋值整个对象的场景在组合式函数中返回响应式数据【导入方式】import{ref}fromvue;【参数说明】value: 任意类型的初始值【返回值】类型Ref对象属性value: 访问或修改响应式值的属性说明在script中需要通过.value访问在template中自动解包【完整示例】script setup import { ref } from vue; // 创建响应式引用 const count ref(0); const message ref(Hello); const isActive ref(true); // 在JavaScript中访问需要.value console.log(count.value); // 0 // 修改值 const increment () { count.value; // 必须通过.value访问和修改 }; // ref也可以存储对象 const user ref({ name: 张三, age: 25 }); // 修改对象属性 user.value.name 李四; /script template !-- 在模板中自动解包不需要.value -- div p计数{{ count }}/p p消息{{ message }}/p p用户{{ user.name }}/p button clickincrement增加/button /div /template关键点在script中访问ref需要.value在template中自动解包不需要.valueref可以存储任何类型的值ref的值是深层响应式的概念2reactive - 对象的响应式API名称reactive【作用】返回对象的响应式代理使对象的所有嵌套属性都具有响应性【使用场景】创建复杂对象的响应式数据需要保持对象引用不变的场景管理多个相关状态【导入方式】import{reactive}fromvue;【参数说明】target: 对象类型Object、Array、Map、Set等注意不能用于基本类型【返回值】类型Proxy对象说明返回原对象的响应式代理所有嵌套属性都是响应式的【完整示例】script setup import { reactive } from vue; // 创建响应式对象 const state reactive({ count: 0, user: { name: 张三, age: 25 }, items: [1, 2, 3] }); // 直接访问和修改不需要.value state.count; state.user.name 李四; state.items.push(4); // 嵌套对象也是响应式的 console.log(state.user.name); // 李四 /script template div p计数{{ state.count }}/p p用户{{ state.user.name }}/p p数组{{ state.items }}/p /div /template关键点reactive只能用于对象、数组、Map、Set等引用类型不能用于基本类型string、number、boolean访问属性不需要.value深层嵌套的对象也会被转换为响应式不能替换整个对象只能修改属性概念3ref vs reactive【对比说明】特性refreactive适用类型任意类型对象类型访问方式需要.value直接访问属性重新赋值可以不可以模板中自动解包直接使用推荐场景基本类型、需要替换的对象复杂对象、多个相关状态【完整示例】script setup import { ref, reactive } from vue; // ✅ ref适合基本类型 const count ref(0); const message ref(Hello); // ✅ reactive适合对象 const user reactive({ name: 张三, age: 25 }); // ❌ 错误reactive不能用于基本类型 // const count reactive(0); // 不会工作 // ✅ ref也可以用于对象 const userRef ref({ name: 李四, age: 30 }); // 访问方式不同 console.log(count.value); // ref需要.value console.log(user.name); // reactive直接访问 // 重新赋值的区别 count.value 10; // ✅ ref可以重新赋值 userRef.value { name: 王五, age: 35 }; // ✅ ref可以替换整个对象 // user { name: 王五 }; // ❌ reactive不能重新赋值 user.name 王五; // ✅ 只能修改属性 /script选择建议基本类型使用ref对象/数组两者都可以但reactive更简洁需要替换整个对象使用ref组合式API推荐统一使用ref保持一致性概念4toRef和toRefsAPI名称toRef / toRefs【作用】toRef: 将reactive对象的单个属性转换为reftoRefs: 将reactive对象的所有属性转换为ref【使用场景】在组合式函数中返回响应式数据时保持响应性解构reactive对象但保持响应性创建对reactive对象属性的响应式引用【导入方式】import{toRef,toRefs}fromvue;【toRef参数说明】object: 响应式对象key: 对象的属性名返回该属性的ref引用【toRefs参数说明】object: 响应式对象返回包含所有属性ref的普通对象【完整示例】script setup import { reactive, toRef, toRefs } from vue; const user reactive({ name: 张三, age: 25, email: zhangsanexample.com }); // toRef转换单个属性 const name toRef(user, name); console.log(name.value); // 张三 name.value 李四; // 会同步修改user.name // toRefs转换所有属性 const { age, email } toRefs(user); console.log(age.value); // 25 age.value 26; // 会同步修改user.age // 使用场景在组合式函数中返回响应式数据 function useUser() { const user reactive({ name: 张三, age: 25 }); // 返回时使用toRefs保持响应性 return { ...toRefs(user) }; } **【关键点】** - toRef和toRefs创建的ref与原对象保持同步 - 修改ref会影响原对象反之亦然 - 适合在组合式函数中使用保持响应性 ### 概念5computed计算属性 **** **API名称computed** **** **【作用】** 创建一个计算属性ref基于其他响应式数据自动计算派生值 **【使用场景】** - 基于现有数据计算派生值 - 需要缓存计算结果的场景 - 复杂的数据转换和过滤 **【导入方式】** javascript import { computed } from vue;【参数说明】方式1只读计算属性getter: 返回计算值的函数返回只读的computed ref方式2可写计算属性options: 包含get和set的对象get(): 返回计算值set(value): 设置值时的处理逻辑返回可读写的computed ref【返回值】类型ComputedRef对象说明返回一个ref访问需要.value具有缓存特性【完整示例】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); // 张三 // 可写计算属性 const fullNameWritable computed({ get() { return ${firstName.value}${lastName.value}; }, set(value) { [firstName.value, lastName.value] value.split(); } }); fullNameWritable.value 李四; // firstName李, lastName四 // 购物车示例 const cart ref([ { id: 1, name: iPhone, price: 5999, quantity: 1 }, { id: 2, name: iPad, price: 3999, quantity: 2 } ]); // 计算总价 const totalPrice computed(() { return cart.value.reduce((sum, item) { return sum item.price * item.quantity; }, 0); }); // 计算商品总数 const totalItems computed(() { return cart.value.reduce((sum, item) sum item.quantity, 0); }); /script template div p全名{{ fullName }}/p p商品总数{{ totalItems }}/p p总价¥{{ totalPrice }}/p /div /templatecomputed的特点基于依赖缓存只有依赖变化时才重新计算返回值是ref需要.value访问适合复杂计算逻辑比方法调用更高效计算函数应该是纯函数不应有副作用概念6watch侦听器API名称watch【作用】侦听一个或多个响应式数据源在数据变化时执行回调函数【使用场景】数据变化时执行异步操作需要访问变化前后的值执行有副作用的操作API调用、DOM操作等【导入方式】import{watch}fromvue;【参数说明】source: 侦听源单个ref单个reactive对象getter函数多个源的数组callback: 回调函数参数(newValue, oldValue, onCleanup)newValue: 新值oldValue: 旧值onCleanup: 清理函数options: 可选配置对象immediate: boolean - 立即执行默认falsedeep: boolean - 深度侦听默认falseflush: ‘pre’ | ‘post’ | ‘sync’ - 回调时机【返回值】类型停止侦听的函数说明调用返回的函数可以停止侦听【完整示例】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}; }); // 侦听多个源 const firstName ref(张); const lastName ref(三); watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) { console.log(姓名从${oldFirst}${oldLast}变为${newFirst}${newLast}); }); // 侦听reactive对象的属性 const user reactive({ name: 张三, age: 25 }); watch( () user.name, // 使用getter函数 (newName, oldName) { console.log(用户名从${oldName}变为${newName}); } ); // 深度侦听 watch( user, (newValue) { console.log(user对象发生变化, newValue); }, { deep: true } // 深度侦听 ); // 立即执行 watch( count, (value) { console.log(当前count:, value); }, { immediate: true } // 立即执行一次 **【关键点】** - watch是懒执行的只在数据变化时触发 - 可以访问新值和旧值 - 侦听reactive对象时newValue和oldValue是相同的引用 - 使用getter函数侦听reactive对象的属性可以获取正确的旧值 ### 概念7watchEffect **** **API名称watchEffect** **** **【作用】** 立即执行一个函数同时响应式地追踪其依赖并在依赖变化时重新执行 **【使用场景】** - 需要立即执行的副作用 - 自动追踪依赖不需要明确指定侦听源 - 不需要访问旧值的场景 **【导入方式】** javascript import { watchEffect } from vue;【参数说明】effect: 副作用函数参数onCleanup- 清理函数函数中使用的响应式数据会被自动追踪options: 可选配置对象flush: ‘pre’ | ‘post’ | ‘sync’ - 回调时机onTrack: 调试用依赖被追踪时调用onTrigger: 调试用依赖变化触发时调用【返回值】类型停止侦听的函数说明调用返回的函数可以停止侦听【完整示例】script setup import { ref, watchEffect } from vue; const count ref(0); const doubled ref(0); // 自动追踪count的变化 watchEffect(() { // 在函数中使用的响应式数据会被自动追踪 doubled.value count.value * 2; console.log(count: ${count.value}, doubled: ${doubled.value}); }); // 停止侦听 const stop watchEffect(() { console.log(count:, count.value); }); // 调用stop()停止侦听 setTimeout(() { stop(); }, 5000); // 清理副作用 watchEffect((onCleanup) { const timer setTimeout(() { console.log(延迟执行); }, 1000); // 在副作用重新执行或组件卸载时清理 onCleanup(() { clearTimeout(timer); }); }); /scriptwatch vs watchEffectwatch需要明确指定侦听源可以访问旧值watchEffect自动追踪依赖更简洁但无法访问旧值watch懒执行只在数据变化时执行watchEffect立即执行然后追踪依赖最佳实践企业级应用场景1. 表单状态管理script setup import { reactive, computed } from vue; const form reactive({ username: , email: , password: , confirmPassword: }); const errors reactive({ username: , email: , password: , confirmPassword: }); // 验证规则 const isValidEmail computed(() { return /^[^\s][^\s]\.[^\s]$/.test(form.email); }); const isPasswordMatch computed(() { return form.password form.confirmPassword; }); const isFormValid computed(() { return form.username isValidEmail.value form.password.length 6 isPasswordMatch.value; }); // 实时验证 watch(() form.email, (value) { if (value !isValidEmail.value) { errors.email 邮箱格式不正确; } else { errors.email ; } }); watch(() form.confirmPassword, (value) { if (value !isPasswordMatch.value) { errors.confirmPassword 两次密码不一致; } else { errors.confirmPassword ; } }); /script2. 数据加载状态script setup import { ref, watchEffect } from vue; const loading ref(false); const data ref(null); const error ref(null); const fetchData async (url) { loading.value true; error.value null; try { const response await fetch(url); data.value await response.json(); } catch (err) { error.value err.message; } finally { loading.value false; } }; // 组合成可复用的函数 function useFetch(url) { const loading ref(false); const data ref(null); const error ref(null); watchEffect(async () { loading.value true; try { const response await fetch(url.value); data.value await response.json(); } catch (err) { error.value err.message; } finally { loading.value false; } }); return { loading, data, error }; } /script常见陷阱陷阱1解构reactive对象丢失响应性script setup import { reactive, toRefs } from vue; const user reactive({ name: 张三, age: 25 }); // ❌ 错误解构后丢失响应性 const { name, age } user; name 李四; // 不会触发更新 // ✅ 正确使用toRefs const { name: nameRef, age: ageRef } toRefs(user); nameRef.value 李四; // 会触发更新 /script陷阱2watch侦听reactive对象script setup import { reactive, watch } from vue; const user reactive({ name: 张三, age: 25 }); // ❌ 问题无法获取旧值 watch(user, (newValue, oldValue) { console.log(newValue oldValue); // true }); // ✅ 正确侦听特定属性 watch( () user.name, (newName, oldName) { console.log(从${oldName}变为${newName}); } ); // ✅ 或者使用深拷贝 watch( () ({ ...user }), (newValue, oldValue) { console.log(新值:, newValue); console.log(旧值:, oldValue); } ); /script陷阱3computed中的副作用script setup import { ref, computed } from vue; const count ref(0); // ❌ 错误computed中不应有副作用 const doubled computed(() { console.log(计算中...); // 副作用 count.value; // 修改其他状态 return count.value * 2; }); // ✅ 正确computed应该是纯函数 const doubledCorrect computed(() { return count.value * 2; }); // 副作用应该放在watch中 watch(count, (value) { console.log(count变化:, value); }); /script性能优化建议1. 避免不必要的响应式script setup import { ref, shallowRef } from vue; // 大型不可变数据使用shallowRef const largeData shallowRef({ // 大量数据... }); // 只有替换整个对象才会触发更新 largeData.value newData; /script2. 使用computed缓存script setup import { ref, computed } from vue; const list ref([/* 大量数据 */]); // ✅ 使用computed缓存计算结果 const filteredList computed(() { return list.value.filter(item item.active); }); // ❌ 避免在模板中直接调用方法 // div v-foritem in filterList() /script实践练习练习1实现购物车难度中等需求描述显示商品列表添加/删除商品修改商品数量计算总价和总数量应用优惠券参考答案script setup import { reactive, computed } from vue; const cart reactive({ items: [ { id: 1, name: iPhone 15, price: 5999, quantity: 1 }, { id: 2, name: iPad Pro, price: 6999, quantity: 1 } ], coupon: { code: , discount: 0 } }); // 计算总价 const subtotal computed(() { return cart.items.reduce((sum, item) { return sum item.price * item.quantity; }, 0); }); // 应用优惠后的总价 const total computed(() { return subtotal.value * (1 - cart.coupon.discount); }); // 商品总数 const totalItems computed(() { return cart.items.reduce((sum, item) sum item.quantity, 0); }); // 添加商品 const addItem (product) { const existingItem cart.items.find(item item.id product.id); if (existingItem) { existingItem.quantity; } else { cart.items.push({ ...product, quantity: 1 }); } }; // 删除商品 const removeItem (id) { const index cart.items.findIndex(item item.id id); if (index -1) { cart.items.splice(index, 1); } }; // 更新数量 const updateQuantity (id, quantity) { const item cart.items.find(item item.id id); if (item) { item.quantity Math.max(1, quantity); } }; // 应用优惠券 const applyCoupon (code) { const coupons { SAVE10: 0.1, SAVE20: 0.2 }; if (coupons[code]) { cart.coupon.code code; cart.coupon.discount coupons[code]; } }; /script template div classcart h2购物车 ({{ totalItems }}件商品)/h2 div v-foritem in cart.items :keyitem.id classcart-item span{{ item.name }}/span span¥{{ item.price }}/span input typenumber :valueitem.quantity inputupdateQuantity(item.id, $event.target.value) min1 / button clickremoveItem(item.id)删除/button /div div classsummary p小计¥{{ subtotal }}/p p v-ifcart.coupon.discount优惠-¥{{ (subtotal * cart.coupon.discount).toFixed(2) }}/p p classtotal总计¥{{ total.toFixed(2) }}/p /div /div /template进阶阅读Vue 3响应式原理Composition API文档响应式最佳实践下一步下一章学习组合式API了解如何组织和复用逻辑代码。返回目录 | 返回总目录