当前位置: 首页 > web >正文

4.3 computed watch watchEffect

在 Vue 3 中,computedwatch(以及 watchEffect)是处理响应式数据的核心工具,它们都能响应数据变化,但设计目的、使用场景和行为特性有显著区别。


一、computed:计算属性(依赖缓存 + 懒执行)

1.1 定义与用途

computed 用于创建基于其他响应式数据派生出的新数据。它是一个“智能工厂”,根据依赖的响应式数据自动计算并返回一个值。

  • 核心特点:自动追踪依赖、具备缓存机制、值是响应式的。
  • 适用场景
    • 根据现有数据计算新值(如总价、反转字符串)。
    • 简化模板中的复杂逻辑。
    • 需要缓存结果以提高性能的计算。
1.2 行为特性
  • 缓存性:只要依赖的数据未变,多次访问 computed 属性会直接返回缓存结果,不会重新执行计算函数。
  • 惰性求值:只有在被访问时才会执行计算(首次访问或依赖变化后)。
  • 同步执行:计算过程必须是同步的。
1.3 使用示例
import { ref, computed } from 'vue';const price = ref(10);
const quantity = ref(5);// 计算总价
const totalPrice = computed(() => {console.log('计算中...'); // 仅当 price 或 quantity 变化时才执行return price.value * quantity.value;
});console.log(totalPrice.value); // 50 (执行计算)
console.log(totalPrice.value); // 50 (直接返回缓存,不打印日志)price.value = 15;
console.log(totalPrice.value); // 75 (依赖变化,重新计算)
1.4 可写计算属性
const number = ref(4);
const squaredNumber = computed({get: () => number.value * number.value,set: (newValue) => {number.value = Math.sqrt(newValue);}
});squaredNumber.value = 25; // 触发 set,number.value 变为 5

二、watch:显式监听(精确控制 + 支持异步)

2.1 定义与用途

watch 用于监听特定数据的变化,并在变化时执行副作用操作(如异步请求、日志打印、复杂逻辑)。

  • 核心特点:明确指定监听源、执行副作用、可配置选项丰富。
  • 适用场景
    • 数据变化时需要执行异步操作(如 API 请求)。
    • 需要获取新值和旧值进行比较。
    • 监听深层对象或数组的变化。
2.2 行为特性
  • 无缓存:每次数据变化都会执行回调函数。
  • 可配置
    • immediate: true:创建时立即执行一次回调。
    • deep: true:深度监听对象/数组内部变化。
  • 返回停止函数:调用返回的函数可停止侦听。
2.3 使用示例
import { ref, watch } from 'vue';const searchQuery = ref('');// 监听 searchQuery 变化,执行搜索
const stopWatch = watch(searchQuery, (newVal, oldVal) => {console.log(`搜索从 "${oldVal}" 变为 "${newVal}"`);if (newVal) {// 模拟异步搜索fetch(`/api/search?q=${newVal}`).then(response => response.json()).then(data => updateResults(data));}
}, {immediate: false, // 是否立即执行deep: false       // 是否深度监听
});// 停止监听
// stopWatch();
2.4 监听多个源
const firstName = ref('');
const lastName = ref('');watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {console.log('姓名变化:', newFirst, newLast);
});

2.5 监听 ref 包裹的复杂对象
import { ref, watch } from 'vue'const state = ref({list: [{ id: 1, name: 'Item 1' },{ id: 2, name: 'Item 2' }]
})watch(() => state.value,(newVal, oldVal) => {console.log('list changed')},{ deep: true }
)// 修改数组内部对象
state.value.list[0].name = 'Updated Item' // 触发

2.6:监听 ref 的值(数组或对象)
const books = ref(['Vue Guide', 'React Guide'])watch(books,(newBooks, oldBooks) => {console.log('books changed')},{ deep: true }
)books.value.push('TypeScript Guide') // 触发

💡 注意:对于 ref,你可以直接传入 ref 本身作为监听源,Vue 会自动解包。

2.7 监听 reactive 对象 
import { reactive } from 'vue';const state = reactive({name: 'Alice',age: 25
});
// 监听整个 reactive 对象(需 deep: true)
watch(state, (newVal, oldVal) => {console.log('state 全局变化:', newVal);
}, { deep: true });// 监听某个具体属性
watch(() => state.name, (newName) => {console.log('姓名变了:', newName);
});

⚠️ 注意:直接 watch(state, ...) 不加 deep: true,只能监听 state 引用变化(一般不会变),无法监听内部属性变化。


⚠️ 注意事项

  1. 性能开销
    deep: true 会递归遍历对象的所有嵌套属性,建立监听依赖,可能带来性能开销,尤其是在监听大型对象时。

  2. oldValue 是代理对象
    使用 deep: true 时,oldVal 是响应式代理(Proxy),修改它会影响原始数据:

    watch(() => user,(newVal, oldVal) => {oldVal.name = 'hacked' // ❌ 不要修改 oldVal!},{ deep: true }
    )
  3. 默认行为

    • 对于 reactive 对象:监听其引用变化,但不会自动深度监听内部嵌套属性(除非你访问了它们)。
    • 对于 ref:监听 .value 的变化。

🆚 与 watchEffect 的对比

特性watch + deep: truewatchEffect
是否需要指定监听源✅ 需要❌ 不需要(自动追踪)
深度监听方式强制递归监听整个对象树只监听实际访问过的属性
性能可能较慢(深度遍历)更高效(精确依赖追踪)
适用场景需要监听整个对象所有变化自动追踪副作用中用到的响应式数据

✅ 最佳实践

  • ✅ 需要监听整个对象/数组的所有变化 → 使用 watch + deep: true
  • ✅ 只关心某些特定属性 → 使用 watch 监听 getter 函数,避免 deep
  • ✅ 副作用逻辑复杂,依赖不明确 → 使用 watchEffect(自动追踪)

示例:避免不必要的 deep

// 更高效的方式:只监听特定字段
watch(() => user.profile.age,(newAge) => {console.log('Age changed to:', newAge)}
)
// 不需要 deep: true

总结

  • deep: true 用于强制 watch 监听对象或数组的所有嵌套属性变化
  • 适用于需要监听复杂结构整体变化的场景。
  • 注意性能影响,优先考虑精确监听或使用 watchEffect 自动追踪依赖。

三、watchEffect:自动监听(副作用优先 + 简化代码)

3.1 定义与用途

watchEffect 会立即执行传入的函数,并自动追踪其内部访问的所有响应式数据作为依赖。当任何依赖变化时,函数会重新执行。

  • 核心特点:自动依赖收集、立即执行、简化代码。
  • 适用场景
    • 副作用逻辑简单且依赖关系明确。
    • 不想手动指定监听源。
3.2 使用示例
import { ref, watchEffect } from 'vue';const count = ref(0);
const enabled = ref(true);watchEffect(() => {if (enabled.value) {console.log('Count is:', count.value); // 自动监听 count 和 enabled}
});count.value++; // 输出: Count is: 1
enabled.value = false;
count.value++; // 无输出(enabled 为 false)
enabled.value = true; // 输出: Count is: 2(重新执行)

四、在组件中如何安全地取消监听(结合 onUnmounted

在 Vue 3 中,无论是 watch 还是 watchEffect,调用它们都会返回一个停止函数(stop function)。调用这个函数即可停止侦听,防止不必要的性能开销或内存泄漏(尤其是在组件卸载前未清理的侦听器)。

在 Vue 组件中,通常建议在组件卸载时自动停止侦听,避免内存泄漏。

💡 最佳实践:如果你在 setup()script setup 中使用 watch/watchEffect,并且侦听的是组件内部状态,通常 Vue 会在组件销毁时自动清理。但如果你侦听的是全局状态、外部响应式对象,或想提前控制,手动调用 stop() 是推荐做法

 什么情况下“必须”清理?

场景是否必须清理说明
监听 windowdocument 事件✅ 必须全局对象,不会自动销毁
使用 watchEffect 读取全局变量✅ 建议清理防止副作用继续执行
监听 Pinia 全局 store⚠️ 通常不需要Pinia 会在组件销毁时自动清理
监听组件内 ref/reactive❌ 不需要Vue 自动管理生命周期
WebSocket / EventSource✅ 必须手动关闭连接,防止内存泄漏

组件卸载时必须调用 stop() 清理的案例

<!-- ResponsivePanel.vue -->
<script setup>
import { ref, watchEffect, onUnmounted } from 'vue';const width = ref(window.innerWidth);
const isMobile = ref(width.value < 768);// 🔴 关键:监听全局 window 事件
const stop = watchEffect(() => {// watchEffect 自动追踪 window.innerWidthwidth.value = window.innerWidth;isMobile.value = width.value < 768;console.log(`窗口宽度更新: ${width.value}px, 移动端: ${isMobile.value}`);// 假设这里还触发了某些 UI 逻辑或 API 请求
});// ✅ 必须在组件卸载时清理!否则内存泄漏
onUnmounted(() => {stop();console.log('✅ window resize 侦听器已清理');
});
</script><template><div class="panel" :class="{ mobile: isMobile, desktop: !isMobile }"><h3>响应式面板</h3><p>当前宽度: {{ width }}px</p><p>设备类型: {{ isMobile ? '移动端' : '桌面端' }}</p></div>
</template><style>
.panel.mobile { background: #ffe4e1; padding: 20px; }
.panel.desktop { background: #e1f5fe; padding: 40px; }
</style>

❌ 如果不清理会发生什么?

假设用户:

  1. 打开 ResponsivePanel 组件(监听开始)
  2. 切换到其他页面,但组件未正确清理 watchEffect
  3. 继续调整窗口大小

👉 后果

  • watchEffect 回调仍会执行(因为 window 事件还在触发)
  • console.log 依然打印
  • 如果里面有 fetchemitstore 操作,会导致:
    • 重复请求 API
    • 更新已销毁组件的状态
    • 内存泄漏(监听器无法被垃圾回收)

WebSocket 全局连接(必须清理)

<!-- LiveChat.vue -->
<script setup>
import { ref, watch, onUnmounted } from 'vue';const messages = ref([]);
const isConnected = ref(false);
let socket = null;// 启动 WebSocket 连接
function connect() {socket = new WebSocket('wss://example.com/chat');socket.onopen = () => {isConnected.value = true;};socket.onmessage = (event) => {const msg = JSON.parse(event.data);messages.value.push(msg);};
}connect();// 🔴 监听全局 WebSocket 消息(虽然是在 setup 中,但 socket 是全局资源)
const stopWatch = watch(() => messages.value.length,(newCount) => {console.log(`收到新消息,总数: ${newCount}`);// 更新通知、播放声音等}
);// ✅ 必须清理:关闭连接 + 停止侦听
onUnmounted(() => {if (socket) {socket.close();console.log('WebSocket 已关闭');}stopWatch(); // 停止 watchconsole.log('✅ LiveChat 侦听器已清理');
});
</script>

⚠️ 如果不调用 socket.close()stopWatch()

  • WebSocket 连接可能保持打开,持续接收消息
  • watch 回调不断执行,试图更新已销毁的组件
  • 严重内存泄漏和性能问题

📌 这就是必须手动清理的原因windowdocumentWebSocketEventSource 等全局对象的事件不会随组件销毁而自动断开。


者对比总结

特性computedwatchwatchEffect
主要用途派生新数据执行副作用(异步/复杂逻辑)执行副作用(自动依赖)
返回值有(响应式引用)无(返回停止函数)无(返回停止函数)
缓存✅ 有缓存❌ 无缓存❌ 无缓存
依赖追踪自动手动指定自动
初始执行访问时执行可配置 immediate立即执行
适用异步❌ 同步✅ 可异步✅ 可异步

五、如何选择?

  • computed

    • 需要根据数据计算出一个,并在模板或多处使用。
    • 计算可能较复杂,需要缓存提升性能。
    • 例如:总价、过滤后的列表、反转字符串。
  • watch

    • 需要在数据变化时执行异步操作(如 API 调用)。
    • 需要精确控制监听源或使用 deep/immediate 选项。
    • 需要比较新旧值。
    • 例如:搜索框防抖请求、表单验证、数据持久化。
  • watchEffect

    • 副作用逻辑简单,且希望自动追踪依赖
    • 不想手动管理依赖列表。
    • 例如:同步状态到外部系统、简单的日志记录。


六、面试高频问题

Q1: computedwatch 的主要区别?

computed 用于计算并返回一个新值,具有缓存,适合同步计算;watch 用于监听变化并执行副作用,适合异步或复杂逻辑。

Q2: 为什么 computed 有缓存而 watch 没有?

computed 的本质是“属性”,其值应与依赖一一对应,缓存避免重复计算提升性能。watch 是“监听器”,每次变化都应触发回调以执行必要的操作。

Q3: watchwatchEffect 如何选择?

当需要明确指定监听源或使用 immediate/deep 时用 watch;当希望自动追踪依赖且逻辑简单时用 watchEffect


正确理解并选择 computedwatchwatchEffect,是编写高效、可维护 Vue 3 应用的关键。

http://www.xdnf.cn/news/17999.html

相关文章:

  • 日本CN2服务器租用多少钱
  • sqli-labs通关笔记-第50关 GET数值型order by堆叠注入(手工注入+脚本注入两种方法)
  • Redis 缓存和 Redis 分布式锁
  • Java 大视界 -- 基于 Java 的大数据可视化在城市交通拥堵治理与出行效率提升中的应用(398)
  • 嵌入式硬件篇---电容滤波
  • Python Sqlalchemy数据库连接
  • 华测科技的3D GPR数据分析
  • 无线也要“自主可控”——东土科技WLAN方案
  • C语言+安全函数+非安全函数
  • imx6ull-驱动开发篇26——Linux 中断实验
  • Pytest 插件使用指南:让你的测试更高效
  • GitHub的使用教程
  • WordPress 7B2主题,在使用PHP 8.0+出现502的解决办法。
  • php危险函数,二.assert()[现版本已弃用]
  • 第十六届蓝桥杯青少组C++省赛[2025.8.9]第二部分编程题(4、矩阵圈层交错旋转)
  • MyBatis 动态数据源切换在 Spring Boot 环境下的实现方案
  • 【postgresql】一文详解postgresql中的统计模块
  • 云手机存储和本地存储的区别
  • C#WPF实战出真汁06--【系统设置】--餐桌类型设置
  • 【总结】Python多线程
  • 国内多光谱相机做得好的厂家有哪些?-多光谱相机品牌厂家
  • 硬件实现webrtc的编解码
  • 【2024前端实战综合练习】HTML+CSS+JavaScript 实现无穷可爱爱心喷射特效(含源代码解析 代码结构逻辑简析)
  • OpenCV 阈值处理
  • Flutter 以模块化方案 适配 HarmonyOS 的实现方法
  • 水环境遥感分析!R语言编程+多源遥感数据预处理;水体指数计算、水深回归分析、水温SVM预测、水质神经网络建模及科研级可视化制图
  • openwrt增加自定义网页
  • LeetCode热题100--146.LRU缓存--中等
  • 第40周——GAN入门
  • 动手学深度学习(pytorch版):第三章节—线性神经网络(4)softmax回归