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

✅ 最佳实践

  • 需要监听整个对象/数组的所有变化 → 使用 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() 是推荐做法

什么情况下"必须"清理?

场景 是否必须清理 说明
监听 window, document 事件 ✅ 必须 全局对象,不会自动销毁
使用 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.innerWidth
  width.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(); // 停止 watch
  console.log('✅ LiveChat 侦听器已清理');
});
</script>

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

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

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


者对比总结

特性 computed watch watchEffect
主要用途 派生新数据 执行副作用(异步/复杂逻辑) 执行副作用(自动依赖)
返回值 有(响应式引用) 无(返回停止函数) 无(返回停止函数)
缓存 ✅ 有缓存 ❌ 无缓存 ❌ 无缓存
依赖追踪 自动 手动指定 自动
初始执行 访问时执行 可配置 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 应用的关键。

相关推荐
持久的棒棒君24 分钟前
启动electron桌面项目控制台输出中文时乱码解决
前端·javascript·electron
小离a_a1 小时前
使用原生css实现word目录样式,标题后面的...动态长度并始终在标题后方(生成点线)
前端·css
郭优秀的笔记2 小时前
抽奖程序web程序
前端·css·css3
布兰妮甜2 小时前
CSS Houdini 与 React 19 调度器:打造极致流畅的网页体验
前端·css·react.js·houdini
小小愿望2 小时前
ECharts 实战技巧:揭秘 X 轴末项标签 “莫名加粗” 之谜及破解之道
前端·echarts
小小愿望2 小时前
移动端浏览器中设置 100vh 却出现滚动条?
前端·javascript·css
fail_to_code2 小时前
请不要再只会回答宏任务和微任务了
前端
摸着石头过河的石头2 小时前
taro3.x-4.x路由拦截如何破?
前端·taro
lpfasd1233 小时前
开发Chrome/Edge插件基本流程
前端·chrome·edge
练习前端两年半3 小时前
🚀 Vue3 源码深度解析:Diff算法的五步优化策略与最长递增子序列的巧妙应用
前端·vue.js