第104课:组合式 API 基础——setup、computed、watch、watchEffect
Vue 3 引入的组合式 API(Composition API)并非要取代选项式 API,而是提供了一种基于函数的逻辑组织方式。当组件变得复杂时,选项式 API 将同一逻辑关注点的代码分散在 data、methods、computed、watch 等不同选项中,导致“逻辑碎片化”。组合式 API 允许你将同一功能的所有响应式状态、计算属性和副作用集中在一个函数中,甚至可以提取为独立的 composable(组合函数)在多个组件间复用。本节课将深入 <script setup> 中 ref 和 reactive 之后的核心工具:computed(计算属性)、watch(侦听器)和 watchEffect(自动追踪副作用),并通过实战对比展示它们与选项式 API 的对应关系及各自的适用场景。
1. computed:计算属性
计算属性用于基于已有响应式状态派生出新值。它有两个核心特性:缓存(依赖不变时不会重新计算)和惰性求值(仅在访问时才计算)。
1.1 基本语法
1 | <script setup> |
与 ref/reactive 的区别:
ref和reactive存储的是独立的状态,它们是数据的“源头”。computed存储的是派生状态,它依赖于其他响应式数据,本身不应被直接赋值(除非定义 setter)。
1.2 计算属性的缓存机制
computed 内部维护了一个 _dirty 标志。当首次访问时计算值并缓存,同时标记 _dirty = false。当依赖的响应式数据发生变化时,_dirty 被重新设为 true,但此时并不会立即重新计算——只有在下次访问 computed.value 时,检测到 _dirty === true 才会执行计算函数。如果依赖变化后从未再访问该计算属性,计算函数永远不会执行。
对比方法调用:如果将 fullName 定义为一个普通函数,每次访问都会重新执行,即使依赖未变。计算属性的缓存特性在依赖的响应式数据频繁变化、计算开销较大时尤为重要。
1 | <script setup> |
1.3 可写计算属性的应用场景
可写计算属性通常用于封装双向绑定逻辑——当用户修改值时,自动拆解并写回原始状态。常见的例子是 v-model 绑定的全名输入框:用户输入全名后,自动拆分为姓和名。
2. watch:显式侦听器
watch 用于显式指定要侦听的响应式数据源,并在数据变化时执行副作用(如发起网络请求、操作 DOM、调用外部库)。与 computed 不同,watch 的回调不返回值,它仅执行副作用。
2.1 侦听单个数据源
1 | <script setup> |
侦听 reactive 对象本身:直接传递 reactive 对象作为 source(而非 getter 函数),watch 会自动启用深度侦听,任何嵌套属性的变化都会触发回调。但此时 newVal 和 oldVal 是同一个引用(因为 reactive 对象在内部被就地修改,而非替换),无法获取旧值。
1 | const state = reactive({ user: { name: 'Alice' } }); |
2.2 侦听多个数据源
1 | const firstName = ref('Zhang'); |
当任一数据源发生变化,回调都会触发。回调的参数是数组形式的新值和旧值。
2.3 watch 的配置选项
1 | watch( |
| 选项 | 说明 |
|---|---|
immediate |
创建侦听器后立即执行一次回调。常用于根据初始值发起请求。 |
deep |
对嵌套对象进行递归遍历,任何深层属性变化都会触发。开销大,慎用。 |
flush |
'pre'(默认,渲染前)、'post'(渲染后)、'sync'(同步,每次变化立即触发)。 |
once |
执行一次后自动移除侦听器。 |
2.4 手动停止侦听器
watch 返回一个停止函数,调用它即可移除侦听器。在组件卸载时,watch 会自动停止(如果在 setup 或 <script setup> 中调用),但在组件外部使用时需要手动停止。
1 | const stop = watch(keyword, callback); |
3. watchEffect:自动追踪依赖的副作用
watchEffect 是 watch 的简化版:不需要显式指定侦听源。它会立即执行一次传入的回调函数,并自动追踪回调内部访问到的所有响应式数据。当这些数据中的任何一个发生变化时,回调会重新执行。
3.1 基本语法
1 | <script setup> |
watchEffect 会自动追踪回调中读取的 userId.value。当 userId 变化时,回调重新执行,再次请求新用户的数据。它不需要像 watch 那样显式列出依赖。
3.2 watchEffect 与 watch 的对比
| 特性 | watch |
watchEffect |
|---|---|---|
| 侦听源 | 必须显式指定(ref、getter、reactive 对象等)。 | 自动追踪回调内访问的所有响应式数据。 |
| 初始执行 | 默认不执行,需设置 immediate: true。 |
立即执行一次。 |
| 获取旧值 | 可以((newVal, oldVal))。 |
不可以,回调不提供旧值。 |
| 适用场景 | 需要精确控制何时触发、需要旧值、需要配置选项。 | 副作用逻辑简单,依赖明确且希望自动追踪。 |
| 性能 | 更精确,可避免不必要的执行。 | 可能追踪到不必要的依赖,需注意回调内的代码。 |
3.3 watchEffect 的清理与停止
watchEffect 的回调可以接收一个 onCleanup 参数,用于注册清理函数。清理函数在回调重新执行之前或侦听器停止时调用,常用于取消未完成的异步请求或清除定时器。
1 | watchEffect((onCleanup) => { |
watchEffect 也返回一个停止函数,与 watch 相同:
1 | const stop = watchEffect(() => { /* ... */ }); |
3.4 watchPostEffect 和 watchSyncEffect
Vue 3 提供了两个 watchEffect 的便捷变体:
- **
watchPostEffect**:回调在 DOM 更新后执行(等同于watchEffect的flush: 'post'),适合需要在 DOM 更新后读取元素尺寸、位置等场景。 watchSyncEffect:回调在响应式数据变化时同步执行(等同于flush: 'sync'),慎用——同步执行可能阻塞渲染,仅在必要时(如需要在状态变化后立即同步读取 DOM)使用。
1 | import { watchPostEffect, watchSyncEffect } from 'vue'; |
4. computed、watch、watchEffect 的选择策略
| 需求 | 推荐工具 |
|---|---|
| 基于已有状态派生一个新值,且需要缓存 | computed |
基于已有状态派生一个新值,且需要支持 v-model 双向绑定 |
computed(可写) |
| 在某个状态变化时执行副作用(网络请求、DOM 操作) | watch |
| 需要获取变化前的旧值 | watch |
| 副作用逻辑中依赖了多个响应式数据,想自动追踪 | watchEffect |
| 需要在组件挂载时立即执行副作用 | watch(immediate: true)或 watchEffect |
5. 综合实战:实时搜索组件
1 | <script setup> |
设计要点:
query是核心状态,驱动所有派生逻辑。hasQuery和resultCount是计算属性,基于query和results派生,且自动缓存。watchEffect自动追踪query.value,发起搜索请求。通过onCleanup取消上一次的请求(AbortController)和定时器(防抖),避免竞态问题。watch用于观察搜索结果数量的变化,执行一些辅助逻辑(如埋点分析)。这展示了watch在需要精确控制监听目标时的优势。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 关于 Vue 3 中的
computed计算属性,以下描述正确的是?
A. 计算属性会在依赖变化时立即重新计算。
B. 计算属性的值被缓存,仅在其响应式依赖变化时重新计算。
C. 计算属性不能定义 setter。
D. 计算属性可以在模板中直接赋值修改。(单选)
watchEffect与watch的主要区别是?
A.watchEffect需要手动指定侦听源。
B.watchEffect可以获取旧值。
C.watchEffect自动追踪回调内访问的响应式数据,并立即执行一次。
D.watchEffect不支持清理函数。(填空) 在
watch中,要获取侦听数据变化前的旧值,需要在回调函数中接收第二个参数______。(多选) 以下哪些是
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在购物车为空时自动隐藏“清空购物车”按钮(通过一个showClearButtonref)。 - 提供添加商品、移除商品、清空购物车的方法。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Vue 3 单文件组件。提示词中需明确指定 computed、watch、watchEffect 的使用场景和参数配置。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建一个 Vue 3 购物车组件,使用
<script setup>和组合式 API。需要创建文件src/components/ShoppingCart.vue:
- 定义两个
ref:products(初始包含 3 个静态商品{ id, name, price })和cart(初始空数组,元素为{ id, name, price, quantity })。computed:
cartTotal:使用reduce计算price * quantity总和,返回保留两位小数的字符串。cartItemCount:计算cart中quantity的总和。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 中
computed、watch和watchEffect的使用场景和内部实现差异。如果一个副作用函数既需要依赖追踪,又需要在依赖变化时获取旧值和新值,应该选择哪个 API?为什么?请给出具体示例。
参考答案:
computed:用于派生状态,计算一个基于其他响应式数据的值。内部有缓存机制,依赖不变时不会重新计算,且惰性求值。适合纯计算场景,不应包含副作用。watch:用于显式侦听特定数据源的变化并执行副作用。可获取新旧值,支持精细配置(immediate、deep、flush)。适合精确控制侦听目标、需要旧值、或需要根据变化执行异步操作的场景。watchEffect:用于自动追踪依赖并执行副作用。立即执行一次,自动收集回调内访问的响应式数据。无需手动声明依赖,但无法获取旧值。适合副作用依赖较复杂但无需精细控制的场景。
如果副作用既需要依赖追踪,又需要新旧值,应选择 watch 配合 getter 函数。因为 watchEffect 不提供旧值。示例:
1 | const state = reactive({ count: 0 }); |
此处用 watch 精确指定侦听目标,避免追踪到不必要的依赖,同时利用回调的 (newVal, oldVal) 参数获取变化前后的值。如果用 watchEffect,需要手动存储旧值,增加不必要的复杂度。
内部实现上,三者都基于 Vue 3 的响应式系统的 effect 函数。computed 内部使用带 lazy 和 scheduler 的 effect,延迟计算并缓存值;watch 内部封装了对 source 的规范化处理和回调调度;watchEffect 是最薄的一层封装,直接将用户回调作为 effect 函数执行。
课后练习答案
一、概念自测答案
B
- 解析:计算属性基于其响应式依赖进行缓存,仅在依赖变化时重新计算。A 错误(不是立即,是惰性);C 错误(可定义 setter);D 错误(模板中不能直接修改计算属性的值)。
C
- 解析:
watchEffect自动追踪依赖并立即执行一次。A 错误(它不需要手动指定);B 错误(它不提供旧值);D 错误(通过onCleanup支持清理函数)。
- 解析:
oldVal(或旧值)- 解析:
watch回调签名为(newVal, oldVal) => {}。
- 解析:
A、B、D
- 解析:
immediate、deep、flush均为watch的合法选项。async不是——异步操作应在回调内部执行,而非通过配置项声明。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请用 Vue 3<script setup>实现一个购物车组件。要求:
ref管理products和cart。computed计算总价和商品总数。watch监听cart(deep: true),将数据存入localStorage,并在初始化时读取。watchEffect根据购物车是否为空控制showClearButton。- 提供
addToCart、removeFromCart、clearCart方法。- 输出完整的单文件组件。”