Vue 3 核心辨析:Computed 与 Watch

在 Vue 的响应式系统中,我们经常面临一个架构选择:当数据 A 发生变化时,我们应该如何更新数据 B,或者触发相应的副作用?

Vue 提供了两个看似相似但本质迥异的 API 来处理这种依赖关系:computed(计算属性)和 watch(侦听器)。

初学者往往容易滥用 watch 来处理所有的数据更新,这会导致代码逻辑混乱且难以维护。它们的区别可以用一句话深刻总结:Computed 是声明式的“状态派生”,而 Watch 是命令式的“副作用执行”。

  1. Computed:智能的缓存机制

computed 的核心理念是 “派生状态” (Derived State)。当你需要基于现有的数据,经过某种计算或处理,得到一个新的结果时,首选 computed。

1.1 核心特性:缓存 (Caching)

这是 computed 与普通函数(Methods)最大的区别。

智能缓存:计算属性会基于其响应式依赖进行缓存。只有当它依赖的数据(例如 count.value)发生改变时,它才会重新求值。

性能优势:如果依赖没有变化,无论你访问多少次 computed 属性,它都会立即返回之前的计算结果,而不会重新执行函数体。这在处理高开销计算(如遍历大数组、复杂数学运算)时至关重要。

1.2 进阶用法:可写计算属性 (Writable Computed)

默认情况下,computed 是只读的。但你也可以通过提供 get 和 set 方法来创建一个“可写”的计算属性,实现数据的双向映射。

const firstName = ref('John');
const lastName = ref('Doe');

const fullName = computed({
// getter: 读取时触发
get() {
return firstName.value + ' ' + lastName.value;
},
// setter: 赋值时触发 (fullName.value = 'David Smith')
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ');
}
});

1.3 最佳实践原则

纯函数:Computed 的 getter 函数应当没有任何副作用。不要在里面发起异步请求、修改 DOM 或修改其他状态。它应该只是单纯地输入数据,输出结果。

避免过度计算:虽然有缓存,但如果计算逻辑非常复杂且依赖变化频繁,仍需考虑性能影响。

  1. Watch:副作用的精准控制

watch 的核心理念是 “响应变化” (Reacting to Changes)。它允许你窥探数据的变化过程,并在变化发生时执行特定的逻辑。

2.1 核心特性:配置与时机

与 computed 不同,watch 不会返回值,它只关心“过程”。

惰性执行:默认情况下,watch 是惰性的,只有当数据源真正发生变化时才会执行回调。

访问旧值:watch 是唯一能让你同时获取“变化前的值”和“变化后的值”的 API,这在需要比较数据差异时非常有用。

2.2 进阶用法:深度侦听与立即执行

watch 提供了第三个参数用于配置高级行为:

deep: true (深度侦听):默认情况下,侦听一个对象(ref 或 reactive)时,只有对象引用改变才会触发。开启 deep: true 后,对象内部嵌套属性的变化(如 user.info.name)也会触发回调。注意:这可能会带来性能开销。

immediate: true (立即执行):强制 watch 在初次绑定时就立即执行一次回调,而不是等待数据变化。常用于页面加载时初始化数据拉取。

watch(source, callback, { deep: true, immediate: true });

2.3 Watch vs WatchEffect

Vue 3 还引入了 watchEffect。它会自动追踪回调函数中用到的所有响应式数据,而不需要显式指定侦听源。它适合那些“依赖项不明确”或“依赖项很多”的副作用场景。

  1. 实战案例:搜索与过滤深度演示

为了全方位展示两者的区别,我们构建一个功能更完善的“用户管理”模块:

Computed:负责前端的列表过滤与统计(纯逻辑)。

Watch:负责防抖搜索(API 请求)与自动保存配置(本地存储)。

3.1 逻辑代码 (JavaScript)

const { ref, computed, watch, reactive } = Vue;

const App = {
setup() {
// --- 1. 数据源 ---
const searchQuery = ref('');
const filters = reactive({ role: 'All', activeOnly: false });
const users = ref([
{ id: 1, name: 'Alice', role: 'Admin', isActive: true },
{ id: 2, name: 'Bob', role: 'User', isActive: false },
{ id: 3, name: 'Charlie', role: 'User', isActive: true },
{ id: 4, name: 'David', role: 'Admin', isActive: true }
]);
const statusLog = ref([]);

    // --- 2. Computed: 负责生成“新数据” ---
    
    // 场景 A: 多重过滤逻辑
    // 只有当 searchQuery, filters 或 users 变化时,才会重新计算
    const filteredUsers = computed(() => {
        console.log('⚡ 触发 Computed 重计算');
        
        return users.value.filter(user => {
            const nameMatch = user.name.toLowerCase().includes(searchQuery.value.toLowerCase());
            const roleMatch = filters.role === 'All' || user.role === filters.role;
            const statusMatch = !filters.activeOnly || user.isActive;
            
            return nameMatch && roleMatch && statusMatch;
        });
    });

    // 场景 B: 动态统计
    // 依赖于 filteredUsers,链式计算
    const resultCount = computed(() => {
        return filteredUsers.value.length;
    });

    // --- 3. Watch: 负责执行“副作用” ---

    // 场景 A: 模拟服务端搜索 (带防抖逻辑)
    // 我们不希望每次输入都发请求,也不希望 computed 变脏
    let timeoutId = null;
    watch(searchQuery, (newVal) => {
        console.log(`👂 Watch 监听到搜索词变化: ${newVal}`);
        
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            // 这里执行真正的 API 调用
            const time = new Date().toLocaleTimeString();
            statusLog.value.unshift(`[${time}] 向服务器发起搜索: "${newVal}"`);
        }, 600); // 600ms 防抖
    });

    // 场景 B: 深度侦听配置对象
    // 当 filters 对象内的任一属性改变时,保存到 localStorage
    watch(filters, (newFilters) => {
        console.log('💾 配置发生变化,正在自动保存...');
        localStorage.setItem('user_filters', JSON.stringify(newFilters));
    }, { deep: true }); // 关键:开启深度侦听

    return {
        searchQuery,
        filters,
        filteredUsers,
        resultCount,
        statusLog
    };
}

};

3.2 模板代码 (HTML Snippet)

高级搜索 (Computed vs Watch)

    <!-- 控制区 -->
    <div class="controls">
        <input v-model="searchQuery" placeholder="输入名字搜索..." class="input-field">
        
        <select v-model="filters.role" class="select-field">
            <option value="All">所有角色</option>
            <option value="Admin">管理员</option>
            <option value="User">普通用户</option>
        </select>
        
        <label>
            <input type="checkbox" v-model="filters.activeOnly"> 仅显示活跃用户
        </label>
    </div>

    <div class="stats">
        找到 {{ resultCount }} 个结果
    </div>

    <div class="content-wrapper">
        <!-- 展示 Computed 结果 -->
        <div class="list-section">
            <h4>用户列表 (Derived State)</h4>
            <ul>
                <li v-for="user in filteredUsers" :key="user.id">
                    {{ user.name }} 
                    <span class="badge" :class="user.role.toLowerCase()">{{ user.role }}</span>
                    <span v-if="!user.isActive" class="inactive">(停用)</span>
                </li>
            </ul>
            <p v-if="resultCount === 0" class="empty-tip">无匹配用户</p>
        </div>

        <!-- 展示 Watch 产生的副作用结果 -->
        <div class="log-section">
            <h4>系统日志 (Side Effects)</h4>
            <ul>
                <li v-for="(log, index) in statusLog" :key="index" class="log-item">
                    {{ log }}
                </li>
            </ul>
        </div>
    </div>
</div>
  1. 核心对比总结与决策指南

为了帮你做最终决定,我们整理了这个详细的决策矩阵:

维度

Computed (计算属性)

Watch (侦听器)

本质

数据的声明式映射 (A -> B)

响应式回调系统 (When A -> Do X)

是否有缓存

✅ 有 (依赖不变不仅算)

❌ 无 (每次触发都执行)

是否支持异步

❌ 不支持 (必须同步返回)

✅ 支持 (可执行 await, setTimeout)

触发时机

获取数据时 (Lazy)

数据变化时 (Eager/Lazy)

配置项

只读 (默认) / 可写 (getter/setter)

immediate, deep, flush

典型误区

在 getter 里修改 data 或 DOM

用 watch 去手动更新另一个 data

⛔️ 警惕反模式 (Anti-Patterns)

错误做法:滥用 Watch 更新数据
http://www.baidu.com/link?url=QntHAophBTi2U2G2lqjyNjqNgDlRt8dlvn8jF1iqHma
http://www.baidu.com/link?url=NwBYGf1uDoyvKWK6M9dTEUd4D8tJScvhRlpN5b_LjQzs9cYRPmXPu62Yo8vYPBy2K4HBeTlK9Z-fcBCDe5x9tq
// ❌ 不要这样做:冗余且容易导致死循环
const fullName = ref('');
watch(firstName, (val) => {
fullName.value = val + ' ' + lastName.value;
});
watch(lastName, (val) => {
fullName.value = firstName.value + ' ' + val;
});

正确做法:使用 Computed

// ✅ 这样做:简洁、高效、自动缓存
const fullName = computed(() => firstName.value + ' ' + lastName.value);

一句话建议:
始终优先考虑 Computed。只有当计算属性无法解决问题(例如需要由状态变化触发网络请求、DOM 操作、或者手动的定时器逻辑)时,才退回到 Watch。

posted @ 2025-12-16 23:39  笑笑学python  阅读(20)  评论(0)    收藏  举报