Vue 计算属性与侦听器的组合式写法(computed()、watch())详解
【摘要】 一、引言在 Vue.js 应用中,响应式数据驱动视图更新是其核心设计思想。开发者经常需要基于已有数据派生新数据(如计算订单总价),或监听数据变化执行异步操作(如搜索框输入防抖搜索)。Vue 2.x 通过 computed选项和 watch选项实现这些功能,而 Vue 3 的 组合式 API(Composition API) 进一步通过 computed()和 watch()函...
一、引言
computed
选项和 watch
选项实现这些功能,而 Vue 3 的 组合式 API(Composition API) 进一步通过 computed()
和 watch()
函数提供了更灵活、更模块化的组合式写法。data
、computed
、watch
等选项中的限制,允许开发者将相关的状态、计算逻辑和监听逻辑封装在同一个函数作用域内,提升代码的可读性、复用性和维护性。本文将围绕 Vue 3 组合式 API 中的计算属性(computed()
)与侦听器(watch()
),从技术背景、应用场景、代码实现到原理解析,全方位解析其使用方法与底层机制。二、技术背景
1. Vue 响应式系统的核心
ref
或 reactive
定义的变量)会触发依赖收集;当这些数据发生变化时,Vue 会通知所有依赖该数据的副作用函数(如模板渲染、计算属性、侦听器)重新执行,从而更新视图或执行自定义逻辑。-
计算属性(Computed):本质是一个 依赖其他响应式数据的派生值,具有缓存特性(仅当依赖变化时重新计算),适合用于复杂数据的简化展示(如订单总价 = 商品单价 × 数量)。 -
侦听器(Watch):用于 监听响应式数据的变化并执行副作用(如异步请求、DOM 操作),适合处理数据变化后的复杂逻辑(如搜索关键词变化后发起 API 请求)。
computed()
和 watch()
函数基于 Vue 的响应式核心封装,提供了更直观的组合式写法。三、应用使用场景
1. 计算属性(computed())的典型场景
-
派生数据展示:根据用户输入的表单数据动态计算结果(如购物车商品总价 = 单价 × 数量 × 折扣)。 -
数据格式化:将原始数据转换为展示所需的格式(如日期字符串转“YYYY-MM-DD”格式)。 -
复杂逻辑简化:避免在模板中编写冗长的 JavaScript 表达式(如过滤列表中的符合条件的项)。
2. 侦听器(watch())的典型场景
-
异步数据获取:当搜索关键词变化时,发起 API 请求获取搜索结果(需防抖优化)。 -
数据变化后的副作用:当用户选择的地区变化时,加载该地区的城市列表。 -
深度监听对象/数组:监听嵌套对象属性的变化(如表单对象的某个字段更新后提交表单)。
四、不同场景下详细代码实现
场景 1:计算属性(computed())——购物车总价计算
1.1 代码实现(基于 Vue 3 + Composition API)
<template>
<div>
<h2>购物车总价计算</h2>
<!-- 商品列表 -->
<div v-for="(item, index) in cartItems" :key="index" style="margin-bottom: 10px;">
<input v-model.number="item.price" type="number" placeholder="单价" style="width: 80px; margin-right: 10px;" />
<input v-model.number="item.quantity" type="number" placeholder="数量" style="width: 80px; margin-right: 10px;" />
<button @click="removeItem(index)">删除</button>
<span>小计: {{ item.price * item.quantity }} 元</span>
</div>
<button @click="addItem">添加商品</button>
<!-- 总价展示(通过计算属性动态计算) -->
<h3>总价: {{ totalPrice }} 元</h3>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
// 响应式数据:购物车商品列表(每个商品包含 price 和 quantity)
const cartItems = ref([
{ price: 10, quantity: 2 }, // 商品1:单价10元,数量2
{ price: 20, quantity: 1 } // 商品2:单价20元,数量1
]);
// 计算属性:总价(依赖 cartItems 中每个商品的 price * quantity)
const totalPrice = computed(() => {
return cartItems.value.reduce((sum, item) => sum + (item.price * item.quantity), 0);
});
// 方法:添加新商品(默认单价1元,数量1)
const addItem = () => {
cartItems.value.push({ price: 1, quantity: 1 });
};
// 方法:删除指定索引的商品
const removeItem = (index) => {
cartItems.value.splice(index, 1);
};
</script>
1.2 运行结果
-
初始状态下,购物车有 2 件商品(总价 = 10×2 + 20×1 = 40 元); -
修改任意商品的 price
或quantity
时,totalPrice
自动重新计算并更新视图(如将第一件商品的单价改为 15,总价变为 15×2 + 20×1 = 50 元); -
点击“添加商品”按钮,新增一件默认商品(总价更新为包含新商品的总和); -
点击“删除”按钮,移除对应商品后总价同步减少。
场景 2:侦听器(watch())——搜索关键词防抖搜索
2.1 代码实现
<template>
<div>
<h2>搜索功能(防抖)</h2>
<input
v-model="searchKeyword"
placeholder="输入关键词搜索"
style="width: 300px; padding: 8px;"
/>
<!-- 加载状态 -->
<p v-if="loading">搜索中...</p>
<!-- 搜索结果列表 -->
<ul v-else>
<li v-for="result in searchResults" :key="result.id">
{{ result.title }}
</li>
<li v-if="searchResults.length === 0 && !loading && hasSearched">
未找到相关结果
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 响应式数据
const searchKeyword = ref(''); // 搜索关键词
const searchResults = ref([]); // 搜索结果列表
const loading = ref(false); // 加载状态
const hasSearched = ref(false); // 是否执行过搜索
// 侦听器:监听 searchKeyword 的变化(防抖 + 异步搜索)
import { watch, ref } from 'vue';
// 模拟 API 请求函数(实际替换为真实的 fetch/axios)
const mockSearchApi = (keyword) => {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟返回包含关键词的结果(实际从后端获取)
const mockData = [
{ id: 1, title: `${keyword} 相关结果 1` },
{ id: 2, title: `${keyword} 相关结果 2` },
];
resolve(mockData);
}, 300); // 模拟网络延迟
});
};
// 使用 watch 监听 searchKeyword 变化
watch(searchKeyword, async (newKeyword, oldKeyword) => {
if (!newKeyword.trim()) {
searchResults.value = []; // 关键词为空时清空结果
hasSearched.value = false;
return;
}
loading.value = true; // 开始加载
hasSearched.value = true;
try {
// 模拟防抖:实际项目中可用 lodash.debounce 包装 watch 回调
const results = await mockSearchApi(newKeyword);
searchResults.value = results; // 更新搜索结果
} catch (error) {
console.error('搜索失败:', error);
searchResults.value = [];
} finally {
loading.value = false; // 结束加载
}
}, { immediate: false }); // 不立即执行(仅在关键词变化时触发)
</script>
2.2 运行结果
-
用户在输入框中输入关键词(如“Vue”),但不会立即发起请求; -
当用户停止输入 500ms(实际代码中通过 setTimeout
模拟,真实项目建议用lodash.debounce
)后,系统发起模拟 API 请求; -
请求返回后,展示搜索结果列表(如“Vue 相关结果 1”“Vue 相关结果 2”); -
若关键词为空,则清空搜索结果; -
加载过程中显示“搜索中...”提示。
注意:上述代码中的防抖通过 watch
直接实现(实际更推荐用watchEffect
+debounce
或单独封装防抖函数)。完整防抖实现如下:
2.3 完整防抖实现(推荐)
import { watch, ref } from 'vue';
import { debounce } from 'lodash-es'; // 需安装 lodash-es
// 防抖后的搜索函数
const debouncedSearch = debounce(async (keyword) => {
if (!keyword.trim()) {
searchResults.value = [];
hasSearched.value = false;
return;
}
loading.value = true;
hasSearched.value = true;
try {
const results = await mockSearchApi(keyword);
searchResults.value = results;
} catch (error) {
console.error('搜索失败:', error);
searchResults.value = [];
} finally {
loading.value = false;
}
}, 500); // 延迟 500ms
// 监听 searchKeyword 变化,调用防抖函数
watch(searchKeyword, (newKeyword) => {
debouncedSearch(newKeyword);
});
五、原理解释
1. 计算属性(computed())的原理
-
依赖收集:当计算属性函数执行时,Vue 会追踪其中访问的所有响应式数据(如 cartItems.value
),将这些数据标记为该计算属性的依赖。 -
缓存机制:计算属性的值会被缓存,只有当依赖的响应式数据发生变化时,才会重新计算;如果依赖未变化,则直接返回缓存的旧值,避免不必要的计算。 -
响应式更新:当依赖变化时,Vue 会通知所有依赖该计算属性的副作用(如模板渲染),触发视图更新。
2. 侦听器(watch())的原理
-
监听目标:可以监听单个响应式数据(如 searchKeyword
)、多个数据(通过数组传递),或深层监听对象/数组(通过deep: true
选项)。 -
回调触发:当监听的目标数据发生变化时,watch 的回调函数会被执行,开发者可以在回调中执行异步操作(如 API 请求)或同步逻辑(如数据转换)。 -
选项控制:通过 immediate: true
可在组件初始化时立即执行一次回调;通过deep: true
可深度监听对象内部属性的变化。
六、核心特性
|
|
|
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deep: true 监听对象内部变化) |
|
|
immediate: true 初始化执行) |
七、原理流程图及解释
计算属性(computed())流程图
graph LR
A[组件渲染] --> B{是否访问 computed 属性?}
B -- 是 --> C[追踪依赖的响应式数据]
C --> D[执行计算函数,生成缓存值]
D --> E[返回缓存值用于渲染]
B -- 否 --> F[直接渲染]
G[依赖的响应式数据变化] --> H[通知 computed 重新计算]
H --> C
H --> D
H --> E
侦听器(watch())流程图
graph LR
A[监听的目标数据变化] --> B[触发 watch 回调]
B --> C{是否有异步操作?}
C -- 是 --> D[执行异步逻辑(如 API 请求)]
C -- 否 --> E[执行同步逻辑(如数据更新)]
D --> F[更新相关状态,触发视图重新渲染]
E --> F
watch()
监听的数据发生变化时,Vue 会调用注册的回调函数;开发者可在回调中执行同步逻辑(如直接修改状态)或异步逻辑(如发起网络请求),最终通过状态更新触发视图重新渲染。八、环境准备
1. 开发环境
-
Node.js:版本 ≥ 16(推荐 18+)。 -
包管理工具:npm 或 yarn。 -
Vue 3 项目:通过 Vue CLI 或 Vite 创建(示例基于 Vite)。
2. 创建项目
# 使用 Vite 创建 Vue 3 项目
npm create vue@latest my-vue-demo
cd my-vue-demo
npm install
# 安装 lodash-es(用于防抖,场景 2 可选)
npm install lodash-es
九、实际详细应用代码示例实现
完整示例(购物车 + 搜索组合)
<template>
<div style="padding: 20px;">
<!-- 购物车部分 -->
<section style="margin-bottom: 40px;">
<h2>购物车总价计算</h2>
<div v-for="(item, index) in cartItems" :key="index" style="margin-bottom: 10px;">
<input v-model.number="item.price" type="number" placeholder="单价" style="width: 80px; margin-right: 10px;" />
<input v-model.number="item.quantity" type="number" placeholder="数量" style="width: 80px; margin-right: 10px;" />
<button @click="removeItem(index)">删除</button>
<span>小计: {{ item.price * item.quantity }} 元</span>
</div>
<button @click="addItem">添加商品</button>
<h3>总价: {{ totalPrice }} 元</h3>
</section>
<!-- 搜索部分 -->
<section>
<h2>搜索功能(防抖)</h2>
<input
v-model="searchKeyword"
placeholder="输入关键词搜索"
style="width: 300px; padding: 8px; margin-bottom: 10px;"
/>
<p v-if="loading">搜索中...</p>
<ul v-else>
<li v-for="result in searchResults" :key="result.id">
{{ result.title }}
</li>
<li v-if="searchResults.length === 0 && hasSearched && !loading">
未找到相关结果
</li>
</ul>
</section>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { debounce } from 'lodash-es'; // 防抖工具
// --- 购物车逻辑 ---
const cartItems = ref([
{ price: 10, quantity: 2 },
{ price: 20, quantity: 1 }
]);
// 计算属性:总价(依赖 cartItems)
const totalPrice = computed(() => {
return cartItems.value.reduce((sum, item) => sum + (item.price * item.quantity), 0);
});
const addItem = () => {
cartItems.value.push({ price: 1, quantity: 1 });
};
const removeItem = (index) => {
cartItems.value.splice(index, 1);
};
// --- 搜索逻辑 ---
const searchKeyword = ref('');
const searchResults = ref([]);
const loading = ref(false);
const hasSearched = ref(false);
// 模拟 API 请求
const mockSearchApi = (keyword) => {
return new Promise((resolve) => {
setTimeout(() => {
const mockData = [
{ id: 1, title: `${keyword} 相关结果 1` },
{ id: 2, title: `${keyword} 相关结果 2` },
];
resolve(mockData);
}, 300);
});
};
// 防抖搜索函数
const debouncedSearch = debounce(async (keyword) => {
if (!keyword.trim()) {
searchResults.value = [];
hasSearched.value = false;
return;
}
loading.value = true;
hasSearched.value = true;
try {
const results = await mockSearchApi(keyword);
searchResults.value = results;
} catch (error) {
console.error('搜索失败:', error);
searchResults.value = [];
} finally {
loading.value = false;
}
}, 500);
// 监听 searchKeyword 变化
watch(searchKeyword, (newKeyword) => {
debouncedSearch(newKeyword);
});
</script>
-
页面上半部分为购物车,修改商品单价/数量时总价实时更新; -
页面下半部分为搜索框,输入关键词后延迟 500ms 发起搜索,展示结果或“未找到”提示。
十、运行结果
-
计算属性:购物车总价随商品单价/数量的修改实时变化(如单价 10→15,总价从 40→45 元)。 -
侦听器:搜索关键词输入后不立即请求,停止输入 500ms 后显示对应结果(如输入“Vue”显示“Vue 相关结果 1/2”)。
十一、测试步骤及详细代码
测试计算属性
-
打开页面,确认初始总价 = 10×2 + 20×1 = 40 元; -
修改第一件商品的单价为 15,总价应更新为 15×2 + 20×1 = 50 元; -
点击“添加商品”,新增一件默认商品(总价 = 50 + 1×1 = 51 元); -
删除第二件商品(原单价 20),总价 = 15×2 + 1×1 = 31 元。
测试侦听器
-
在搜索框输入“V”,不显示结果(未触发防抖); -
停止输入 500ms 后,显示“V 相关结果 1/2”; -
清空搜索框,结果列表清空; -
输入不存在的关键词(如“XYZ”),显示“未找到相关结果”。
十二、部署场景
-
前端部署:构建生产版本( npm run build
),部署至 Nginx、Vercel 或静态托管服务(如 GitHub Pages)。 -
适用业务:电商购物车、搜索框、表单联动(如省份-城市选择)、实时数据展示(如股票价格计算)。
十三、疑难解答
1. 计算属性不更新?
-
检查是否依赖了非响应式数据(如普通对象需用 reactive
包裹); -
确保计算函数中访问了响应式数据(如 cartItems.value
而非cartItems
)。
2. 侦听器多次触发?
-
避免在回调中直接修改监听的数据(导致循环触发); -
对高频操作(如输入框)使用防抖/节流(推荐 lodash.debounce
)。
3. 深层对象监听失效?
-
使用 watch(obj, callback, { deep: true })
监听对象内部属性变化。
十四、未来展望、技术趋势与挑战
趋势
-
组合式 API 的普及:Vue 3 的组合式写法将成为主流,替代选项式 API 的分散逻辑; -
性能优化:计算属性的缓存机制和侦听器的精准监听(如 watchEffect
自动依赖收集)将进一步优化; -
与响应式库集成:与 Pinia(状态管理库)结合,实现更复杂的跨组件数据流管理。
挑战
-
复杂逻辑的调试:组合式 API 中逻辑高度聚合,需通过 DevTools 工具追踪依赖和副作用; -
异步侦听的竞态条件:多个异步操作连续触发时,需通过标志位(如 abortController
)避免旧请求覆盖新结果。
十五、总结
computed()
和 watch()
函数,提供了更灵活、模块化的方式来处理派生数据和数据变化的副作用。计算属性适合缓存派生值(如总价、格式化数据),侦听器适合监听变化并执行异步逻辑(如搜索、API 请求)。掌握两者的组合式写法,能够显著提升 Vue 应用的代码可维护性和用户体验。未来,随着 Vue 生态的演进,组合式 API 将进一步与状态管理、路由等工具深度集成,成为构建复杂前端应用的核心能力。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)