WinddSnow

Vue-3-Composition-API-Basics-setup-computed-watch-watchEffect

字数统计: 4.3k阅读时长: 17 min
2026/08/01

第104课:组合式 API 基础——setupcomputedwatchwatchEffect

Vue 3 引入的组合式 API(Composition API)并非要取代选项式 API,而是提供了一种基于函数的逻辑组织方式。当组件变得复杂时,选项式 API 将同一逻辑关注点的代码分散在 datamethodscomputedwatch 等不同选项中,导致“逻辑碎片化”。组合式 API 允许你将同一功能的所有响应式状态、计算属性和副作用集中在一个函数中,甚至可以提取为独立的 composable(组合函数)在多个组件间复用。本节课将深入 <script setup>refreactive 之后的核心工具:computed(计算属性)、watch(侦听器)和 watchEffect(自动追踪副作用),并通过实战对比展示它们与选项式 API 的对应关系及各自的适用场景。


1. computed:计算属性

计算属性用于基于已有响应式状态派生出新值。它有两个核心特性:缓存(依赖不变时不会重新计算)和惰性求值(仅在访问时才计算)。

1.1 基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script setup>
import { ref, computed } from 'vue';

const firstName = ref('Zhang');
const lastName = ref('San');

// 只读计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});

// 可写计算属性
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(newValue) {
const parts = newValue.split(' ');
firstName.value = parts[0] || '';
lastName.value = parts[1] || '';
}
});
</script>

<template>
<p>{{ fullName }}</p>
<input v-model="fullNameWritable" />
</template>

ref/reactive 的区别

  • refreactive 存储的是独立的状态,它们是数据的“源头”。
  • computed 存储的是派生状态,它依赖于其他响应式数据,本身不应被直接赋值(除非定义 setter)。

1.2 计算属性的缓存机制

computed 内部维护了一个 _dirty 标志。当首次访问时计算值并缓存,同时标记 _dirty = false。当依赖的响应式数据发生变化时,_dirty 被重新设为 true,但此时并不会立即重新计算——只有在下次访问 computed.value 时,检测到 _dirty === true 才会执行计算函数。如果依赖变化后从未再访问该计算属性,计算函数永远不会执行。

对比方法调用:如果将 fullName 定义为一个普通函数,每次访问都会重新执行,即使依赖未变。计算属性的缓存特性在依赖的响应式数据频繁变化、计算开销较大时尤为重要。

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref, computed } from 'vue';

const items = ref([...Array(1000)].map((_, i) => ({ id: i, value: Math.random() })));

// computed:仅在 items 变化时重新计算
const expensiveResult = computed(() => {
console.log('computing...');
return items.value.filter(i => i.value > 0.5).length;
});
</script>

1.3 可写计算属性的应用场景

可写计算属性通常用于封装双向绑定逻辑——当用户修改值时,自动拆解并写回原始状态。常见的例子是 v-model 绑定的全名输入框:用户输入全名后,自动拆分为姓和名。


2. watch:显式侦听器

watch 用于显式指定要侦听的响应式数据源,并在数据变化时执行副作用(如发起网络请求、操作 DOM、调用外部库)。与 computed 不同,watch 的回调不返回值,它仅执行副作用。

2.1 侦听单个数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<script setup>
import { ref, watch } from 'vue';

const keyword = ref('');

// 侦听 ref
watch(keyword, (newVal, oldVal) => {
console.log(`搜索关键词从 "${oldVal}" 变为 "${newVal}"`);
});

// 侦听 getter 函数
const user = ref({ name: 'Alice', age: 25 });
watch(
() => user.value.age,
(newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`);
}
);

// 侦听 reactive 对象的属性
import { reactive } from 'vue';
const state = reactive({ count: 0 });
watch(
() => state.count,
(newCount) => {
console.log('count changed to', newCount);
}
);
</script>

侦听 reactive 对象本身:直接传递 reactive 对象作为 source(而非 getter 函数),watch自动启用深度侦听,任何嵌套属性的变化都会触发回调。但此时 newValoldVal同一个引用(因为 reactive 对象在内部被就地修改,而非替换),无法获取旧值。

1
2
3
4
const state = reactive({ user: { name: 'Alice' } });
watch(state, (newVal, oldVal) => {
console.log(newVal === oldVal); // true!因为是同一个 Proxy 对象
});

2.2 侦听多个数据源

1
2
3
4
5
6
7
8
9
const firstName = ref('Zhang');
const lastName = ref('San');

watch(
[firstName, lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名从 "${oldFirst} ${oldLast}" 变为 "${newFirst} ${newLast}"`);
}
);

当任一数据源发生变化,回调都会触发。回调的参数是数组形式的新值和旧值。

2.3 watch 的配置选项

1
2
3
4
5
6
7
8
9
10
watch(
source,
callback,
{
immediate: true, // 立即执行一次回调(使用当前值)
deep: true, // 深度侦听(对 ref 对象或 getter 返回的对象)
flush: 'post', // 回调在 DOM 更新后执行(默认 'pre')
once: true, // (Vue 3.4+) 仅侦听一次,之后自动停止
}
);
选项 说明
immediate 创建侦听器后立即执行一次回调。常用于根据初始值发起请求。
deep 对嵌套对象进行递归遍历,任何深层属性变化都会触发。开销大,慎用。
flush 'pre'(默认,渲染前)、'post'(渲染后)、'sync'(同步,每次变化立即触发)。
once 执行一次后自动移除侦听器。

2.4 手动停止侦听器

watch 返回一个停止函数,调用它即可移除侦听器。在组件卸载时,watch 会自动停止(如果在 setup<script setup> 中调用),但在组件外部使用时需要手动停止。

1
2
3
const stop = watch(keyword, callback);
// 手动停止
stop();

3. watchEffect:自动追踪依赖的副作用

watchEffectwatch 的简化版:不需要显式指定侦听源。它会立即执行一次传入的回调函数,并自动追踪回调内部访问到的所有响应式数据。当这些数据中的任何一个发生变化时,回调会重新执行。

3.1 基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { ref, watchEffect } from 'vue';

const userId = ref(1);
const user = ref(null);

// 无需显式声明依赖
watchEffect(async () => {
// 自动追踪 userId.value 和 user.value
const response = await fetch(`/api/users/${userId.value}`);
user.value = await response.json();
console.log('获取用户数据:', user.value);
});
</script>

watchEffect 会自动追踪回调中读取的 userId.value。当 userId 变化时,回调重新执行,再次请求新用户的数据。它不需要像 watch 那样显式列出依赖。

3.2 watchEffectwatch 的对比

特性 watch watchEffect
侦听源 必须显式指定(ref、getter、reactive 对象等)。 自动追踪回调内访问的所有响应式数据。
初始执行 默认不执行,需设置 immediate: true 立即执行一次
获取旧值 可以((newVal, oldVal))。 不可以,回调不提供旧值。
适用场景 需要精确控制何时触发、需要旧值、需要配置选项。 副作用逻辑简单,依赖明确且希望自动追踪。
性能 更精确,可避免不必要的执行。 可能追踪到不必要的依赖,需注意回调内的代码。

3.3 watchEffect 的清理与停止

watchEffect 的回调可以接收一个 onCleanup 参数,用于注册清理函数。清理函数在回调重新执行之前侦听器停止时调用,常用于取消未完成的异步请求或清除定时器。

1
2
3
4
5
6
7
8
watchEffect((onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort()); // 下一次执行前取消上一次请求

fetch(`/api/users/${userId.value}`, { signal: controller.signal })
.then(res => res.json())
.then(data => { user.value = data; });
});

watchEffect 也返回一个停止函数,与 watch 相同:

1
2
const stop = watchEffect(() => { /* ... */ });
stop();

3.4 watchPostEffectwatchSyncEffect

Vue 3 提供了两个 watchEffect 的便捷变体:

  • **watchPostEffect**:回调在 DOM 更新后执行(等同于 watchEffectflush: 'post'),适合需要在 DOM 更新后读取元素尺寸、位置等场景。
  • watchSyncEffect:回调在响应式数据变化时同步执行(等同于 flush: 'sync'),慎用——同步执行可能阻塞渲染,仅在必要时(如需要在状态变化后立即同步读取 DOM)使用。
1
import { watchPostEffect, watchSyncEffect } from 'vue';

4. computedwatchwatchEffect 的选择策略

需求 推荐工具
基于已有状态派生一个新值,且需要缓存 computed
基于已有状态派生一个新值,且需要支持 v-model 双向绑定 computed(可写)
在某个状态变化时执行副作用(网络请求、DOM 操作) watch
需要获取变化前的旧值 watch
副作用逻辑中依赖了多个响应式数据,想自动追踪 watchEffect
需要在组件挂载时立即执行副作用 watchimmediate: true)或 watchEffect

5. 综合实战:实时搜索组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<script setup>
import { ref, computed, watch, watchEffect } from 'vue';

const query = ref('');
const results = ref([]);
const loading = ref(false);
const error = ref(null);

// computed:派生一个“是否有搜索词”的布尔值
const hasQuery = computed(() => query.value.trim().length > 0);

// computed:搜索结果计数
const resultCount = computed(() => results.value.length);

// watchEffect:自动追踪 query,发起请求并处理竞态
watchEffect((onCleanup) => {
if (!hasQuery.value) {
results.value = [];
return;
}

loading.value = true;
error.value = null;
const controller = new AbortController();
onCleanup(() => controller.abort());

// 模拟 API 请求
const timer = setTimeout(async () => {
try {
// 实际项目中替换为 fetch
const mockData = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Apricot' },
].filter(item => item.name.toLowerCase().includes(query.value.toLowerCase()));
results.value = mockData;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}, 300); // 300ms 防抖

onCleanup(() => clearTimeout(timer));
});

// watch:监听搜索结果数量变化,执行分析(仅示例)
watch(resultCount, (newCount) => {
if (newCount > 0) {
console.log(`搜索到 ${newCount} 条结果`);
}
});
</script>

<template>
<div>
<input v-model="query" placeholder="搜索..." />
<span v-if="loading">加载中...</span>
<span v-if="error" style="color: red;">{{ error }}</span>

<p v-if="hasQuery">共 {{ resultCount }} 条结果</p>

<ul v-if="results.length">
<li v-for="item in results" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>

设计要点

  • query 是核心状态,驱动所有派生逻辑。
  • hasQueryresultCount 是计算属性,基于 queryresults 派生,且自动缓存。
  • watchEffect 自动追踪 query.value,发起搜索请求。通过 onCleanup 取消上一次的请求(AbortController)和定时器(防抖),避免竞态问题。
  • watch 用于观察搜索结果数量的变化,执行一些辅助逻辑(如埋点分析)。这展示了 watch 在需要精确控制监听目标时的优势。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 关于 Vue 3 中的 computed 计算属性,以下描述正确的是?
    A. 计算属性会在依赖变化时立即重新计算。
    B. 计算属性的值被缓存,仅在其响应式依赖变化时重新计算。
    C. 计算属性不能定义 setter。
    D. 计算属性可以在模板中直接赋值修改。

  2. (单选) watchEffectwatch 的主要区别是?
    A. watchEffect 需要手动指定侦听源。
    B. watchEffect 可以获取旧值。
    C. watchEffect 自动追踪回调内访问的响应式数据,并立即执行一次。
    D. watchEffect 不支持清理函数。

  3. (填空)watch 中,要获取侦听数据变化前的旧值,需要在回调函数中接收第二个参数 ______

  4. (多选) 以下哪些是 watch 的合法配置选项?
    A. immediate: true
    B. deep: true
    C. async: true
    D. flush: 'post'

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要实现一个购物车组件,使用 Vue 3 组合式 API。要求如下:

  • 使用 ref 管理商品列表和购物车列表。
  • 使用 computed 计算购物车总价和商品总数。
  • 使用 watch 监听购物车变化,将其保存到 localStorage(并指定 deep: true)。
  • 使用 watchEffect 在购物车为空时自动隐藏“清空购物车”按钮(通过一个 showClearButton ref)。
  • 提供添加商品、移除商品、清空购物车的方法。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Vue 3 单文件组件。提示词中需明确指定 computedwatchwatchEffect 的使用场景和参数配置。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个 Vue 3 购物车组件,使用 <script setup> 和组合式 API。需要创建文件 src/components/ShoppingCart.vue

  • 定义两个 refproducts(初始包含 3 个静态商品 { id, name, price })和 cart(初始空数组,元素为 { id, name, price, quantity })。
  • computed
    • cartTotal:使用 reduce 计算 price * quantity 总和,返回保留两位小数的字符串。
    • cartItemCount:计算 cartquantity 的总和。
  • watch:监听 cart,设置 { deep: true },在回调中将 JSON.stringify(cart.value) 存入 localStorage'cart' 键。同时从 localStorage 读取初始值(通过惰性初始化 ref)。
  • watchEffect:根据 cart.value.length === 0 设置 showClearButton 的值为 false,否则为 true
  • 方法:addToCart(product)(若已在购物车则增加数量,否则 push)、removeFromCart(id)(过滤)、clearCart()(重置为空数组)。
  • 模板:左侧展示商品列表,每项有“加入购物车”按钮;右侧展示购物车列表,每项有数量显示和“移除”按钮;底部显示总价和“清空购物车”按钮(条件渲染)。
  • 所有代码添加 JSDoc 注释。完成后输出完整文件内容。

四、面试真题与参考答案

题目(美团前端面试题):

请对比 Vue 3 中 computedwatchwatchEffect 的使用场景和内部实现差异。如果一个副作用函数既需要依赖追踪,又需要在依赖变化时获取旧值和新值,应该选择哪个 API?为什么?请给出具体示例。

参考答案

  • computed:用于派生状态,计算一个基于其他响应式数据的值。内部有缓存机制,依赖不变时不会重新计算,且惰性求值。适合纯计算场景,不应包含副作用。
  • watch:用于显式侦听特定数据源的变化并执行副作用。可获取新旧值,支持精细配置(immediatedeepflush)。适合精确控制侦听目标、需要旧值、或需要根据变化执行异步操作的场景。
  • watchEffect:用于自动追踪依赖并执行副作用。立即执行一次,自动收集回调内访问的响应式数据。无需手动声明依赖,但无法获取旧值。适合副作用依赖较复杂但无需精细控制的场景。

如果副作用既需要依赖追踪,又需要新旧值,应选择 watch 配合 getter 函数。因为 watchEffect 不提供旧值。示例:

1
2
3
4
5
6
7
const state = reactive({ count: 0 });
watch(
() => state.count, // 精确侦听 state.count
(newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`);
}
);

此处用 watch 精确指定侦听目标,避免追踪到不必要的依赖,同时利用回调的 (newVal, oldVal) 参数获取变化前后的值。如果用 watchEffect,需要手动存储旧值,增加不必要的复杂度。

内部实现上,三者都基于 Vue 3 的响应式系统的 effect 函数。computed 内部使用带 lazyschedulereffect,延迟计算并缓存值;watch 内部封装了对 source 的规范化处理和回调调度;watchEffect 是最薄的一层封装,直接将用户回调作为 effect 函数执行。


课后练习答案

一、概念自测答案

  1. B

    • 解析:计算属性基于其响应式依赖进行缓存,仅在依赖变化时重新计算。A 错误(不是立即,是惰性);C 错误(可定义 setter);D 错误(模板中不能直接修改计算属性的值)。
  2. C

    • 解析:watchEffect 自动追踪依赖并立即执行一次。A 错误(它不需要手动指定);B 错误(它不提供旧值);D 错误(通过 onCleanup 支持清理函数)。
  3. oldVal(或旧值

    • 解析:watch 回调签名为 (newVal, oldVal) => {}
  4. A、B、D

    • 解析:immediatedeepflush 均为 watch 的合法选项。async 不是——异步操作应在回调内部执行,而非通过配置项声明。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请用 Vue 3 <script setup> 实现一个购物车组件。要求:

  • ref 管理 productscart
  • computed 计算总价和商品总数。
  • watch 监听 cartdeep: true),将数据存入 localStorage,并在初始化时读取。
  • watchEffect 根据购物车是否为空控制 showClearButton
  • 提供 addToCartremoveFromCartclearCart 方法。
  • 输出完整的单文件组件。”
CATALOG
  1. 1. 第104课:组合式 API 基础——setup、computed、watch、watchEffect
    1. 1.1. 1. computed:计算属性
      1. 1.1.1. 1.1 基本语法
      2. 1.1.2. 1.2 计算属性的缓存机制
      3. 1.1.3. 1.3 可写计算属性的应用场景
    2. 1.2. 2. watch:显式侦听器
      1. 1.2.1. 2.1 侦听单个数据源
      2. 1.2.2. 2.2 侦听多个数据源
      3. 1.2.3. 2.3 watch 的配置选项
      4. 1.2.4. 2.4 手动停止侦听器
    3. 1.3. 3. watchEffect:自动追踪依赖的副作用
      1. 1.3.1. 3.1 基本语法
      2. 1.3.2. 3.2 watchEffect 与 watch 的对比
      3. 1.3.3. 3.3 watchEffect 的清理与停止
      4. 1.3.4. 3.4 watchPostEffect 和 watchSyncEffect
    4. 1.4. 4. computed、watch、watchEffect 的选择策略
    5. 1.5. 5. 综合实战:实时搜索组件
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)