虽然在前面的课程中我们学习了如何在模板中使用表达式,但在某些情况下,模板中的表达式可能会变得复杂,或者我们需要在多个地方使用相同的逻辑。Vue提供了计算属性(computed)和侦听器(watch)来处理这些场景。
计算属性用于基于响应式数据计算派生值,而侦听器用于在数据变化时执行副作用操作。在这一堂课中,我们将学习它们的用法,了解它们之间的区别,以及如何在实际项目中正确使用它们。

让我们先看一个例子,理解为什么需要计算属性。假设我们有一个购物车,需要显示商品的总价:
|<div id="app"> <div v-for="item in cart" :key="item.id"> {{ item.name }} - ¥{{ item.price }} × {{ item.quantity }} </div> <p>总价:¥{{ cart.reduce((sum, item) => sum + item.price * item.quantity, 0) }}</p> </div> <script> const { createApp, ref } = Vue; const app = createApp({ setup() { const cart = ref([ { id: 1, name: '商品1', price: 100, quantity: 2 }, { id: 2, name: '商品2', price: 200, quantity: 1 } ]); return { cart }; } }); app.mount('#app'); </script>
在这个例子中,我们在模板中直接写了一个复杂的表达式来计算总价。虽然这样可以工作,但有几个问题:首先,代码可读性差;其次,如果我们在多个地方需要使用总价,就需要重复这个表达式;最后,每次组件重新渲染时,这个表达式都会重新计算,即使cart没有变化。
使用计算属性可以解决这些问题:
|<div id="app"> <div v-for="item in cart" :key="item.id"> {{ item.name }} - ¥{{ item.price }} × {{ item.quantity }} </div> <p>总价:¥{{ totalPrice }}</p> </div> <script> const { createApp, ref, computed } = Vue;
现在,总价的计算逻辑被封装在totalPrice计算属性中。代码更清晰,而且计算属性会缓存结果,只有当依赖的数据(cart)发生变化时才会重新计算。
在组合式API中,我们使用computed函数来创建计算属性。computed接收一个函数,这个函数返回计算后的值:
|<div id="app"> <p>名字:{{ firstName }}</p> <p>姓氏:{{ lastName }}</p> <p>全名:{{ fullName }}</p> </div> <script> const { createApp, ref, computed } = Vue; const app =
在这个例子中,fullName是一个计算属性,它基于firstName和lastName计算全名。当firstName或lastName改变时,fullName会自动更新。
计算属性是响应式的,我们可以在模板中像使用普通数据一样使用它。计算属性也是缓存的,只有当它的依赖发生变化时才会重新计算。
你可能会问,为什么不用方法来实现同样的功能呢?其实,计算属性(computed)和方法(methods)虽然都能实现类似的动态计算,但它们在行为和性能上有重要区别:
下面我们通过代码示例,对比计算属性和方法的用法和效果:
|<div id="app"> <p>计算属性:{{ reversedMessage }}</p> <p>计算属性:{{ reversedMessage }}</p> <p>方法:{{ reverseMessage() }}</p> <p>方法:{{ reverseMessage() }}</p> </div> <script> const { createApp, ref, computed } =
如果你运行这段代码,你会看到"计算属性被调用"只打印一次,而"方法被调用"会打印两次。这是因为计算属性会缓存结果,只有当依赖的数据变化时才会重新计算;而方法每次调用都会执行。
默认情况下,计算属性是只读的——你只能读取它们的值,不能直接修改它们。但如果我们想让计算属性既能读取也能设置(即“可写”),就需要为它定义 getter(读取函数)和 setter(设置函数)。 getter 决定了我们在访问计算属性时返回什么结果,setter 决定了在给计算属性赋值时该如何处理这个值。这样,计算属性就像是某个数据的“复合视图”,它的读取和修改都可以自定义逻辑。下面我们来看具体怎么实现:
|<div id="app"> <p>名字:{{ firstName }}</p> <p>姓氏:{{ lastName }}</p> <p>全名:{{ fullName }}</p> <input v-model="fullName" placeholder="修改全名"> </div> <script> const { createApp, ref
在这个例子中,我们为fullName提供了getter和setter。getter返回全名,setter根据新值更新firstName和lastName。现在,我们可以通过v-model直接绑定到fullName,修改全名会自动更新名字和姓氏。
在实际开发中,很多场景下我们需要在某个数据发生变化时“做点事情”,比如:用户输入后自动进行搜索、表单值更新时校验、某个状态变化时去请求新数据、或者某些变量变动时手动更新页面中的某个部分。
此时,watch 函数就非常有用。它可以用来“侦听”一个或多个响应式数据的变化——每当这些数据发生改变时,会自动执行我们指定的回调函数。
这样的机制可以让你灵活地插入副作用操作:比如调用接口、操作 DOM、节流防抖、写日志等等。通过 watch,你可以把“某个数据变化时需要联动的逻辑”与其它业务代码很好地解耦,让你的逻辑更清晰、更易维护。
让我们看一个简单的例子:
|<div id="app"> <input v-model="searchQuery" placeholder="搜索"> <p>搜索关键词:{{ searchQuery }}</p> </div> <script> const { createApp, ref, watch } = Vue; const app = createApp
在这个例子中,我们使用watch来侦听searchQuery的变化。当searchQuery的值改变时,回调函数会被调用,我们可以在这里执行搜索操作。
watch的第一个参数是要侦听的数据,第二个参数是回调函数。回调函数接收两个参数:新值和旧值。
我们也可以同时侦听多个数据源,当任何一个数据源发生变化时,回调函数都会被调用。
|<div id="app"> <input v-model="firstName" placeholder="名字"> <input v-model="lastName" placeholder="姓氏"> <p>全名:{{ firstName }} {{ lastName }}</p> </div> <script> const { createApp, ref,
在这个例子中,我们使用数组来同时侦听firstName和lastName。回调函数接收两个数组:新值数组和旧值数组。
默认情况下,watch 只会侦听被监视数据的最外层(顶层)的变化,也就是说,仅当对这个对象或数组的引用本身发生改变时才会触发回调,而不会对对象中的嵌套属性或数组内部的元素进行追踪。例如,如果你对对象的某个属性赋新值,或者向数组里添加/删除元素,watch 默认是感知不到这些深层次变化的。
如果我们要监听对象或数组内部属性的任何变化(比如对象的属性被修改、数组内容被修改),这时就需要启用“深度侦听”功能。具体做法是给 watch 的第三个参数传递 { deep: true },这样无论对象哪一层属性或数组里的哪一项发生变化,回调都会被调用。
下面我们看一个相关的例子:
|<div id="app"> <input v-model="user.name" placeholder="名字"> <input v-model="user.age" placeholder="年龄"> <p>用户信息:{{ user.name }},{{ user.age }}岁</p> </div> <script> const { createApp, reactive,
在这个例子中,我们使用{ deep: true }选项来启用深度侦听。现在,当user.name或user.age改变时,watch都会触发。
注意,对于reactive对象,深度侦听是默认启用的。但对于ref对象,我们需要显式启用:
|<script> const { createApp, ref, watch } = Vue; const app = createApp({ setup() { const user = ref({ name: '张三', age: 25 }); watch(user, (newValue, oldValue) =>
默认情况下,watch只在数据变化时执行。如果我们希望在初始化时也执行一次,可以使用{ immediate: true }选项:
|<div id="app"> <input v-model="searchQuery" placeholder="搜索"> </div> <script> const { createApp, ref, watch } = Vue; const app = createApp({ setup() { const searchQuery
在这个例子中,watch会在组件初始化时立即执行一次,然后每次searchQuery变化时也会执行。
watchEffect 是 Vue 3 新增的一个响应式侦听 API。它的作用是:你只需编写一个回调函数,在这个函数内部用到的所有响应式数据(ref、reactive 等)都会被 Vue 自动追踪,一旦这些数据发生变化,回调就会重新执行。
也就是说,watchEffect 不需要你手动指定侦听的对象,它会自动收集依赖。
每当你在 watchEffect 的回调内访问某个响应式属性,Vue 就会把这些属性关联到这个回调。当这些响应式数据变化时,watchEffect 注册的回调会立即再次运行。
并且,watchEffect 在初始化时会立即执行一次回调,这和 watch 的 { immediate: true } 效果类似。
这个 API 特别适合需要根据多项响应式数据自动计算、同步副作用或执行逻辑的场景。比如自动做网络请求、同步派生状态等操作都很方便,相比 watch,使用更加简洁。
|<div id="app"> <input v-model="count" type="number"> <p>计数:{{ count }}</p> <p>双倍计数:{{ doubleCount }}</p> </div> <script> const { createApp, ref, watchEffect } = Vue;
在这个例子中,watchEffect会自动追踪count,当count变化时,回调函数会重新执行,更新doubleCount。
watchEffect和watch的区别在于,watchEffect不需要显式指定要侦听的数据,它会自动追踪回调函数中使用的所有响应式数据。watchEffect也会在初始化时立即执行一次。
watch和watchEffect各有优缺点。watch更明确,可以精确控制要侦听的数据;watchEffect更简洁,会自动追踪依赖。
一般来说,如果你需要侦听特定的数据,使用watch;如果你需要自动追踪依赖,使用watchEffect。
让我们看一个使用watchEffect的例子:
|<div id="app"> <input v-model="searchQuery" placeholder="搜索"> <div v-if="loading">加载中...</div> <div v-else>搜索结果:{{ results }}</div> </div> <script> const { createApp, ref,
在这个例子中,watchEffect会自动追踪searchQuery,当它变化时,会自动执行搜索操作。
有时候,在某些场景下我们不希望一直侦听某个数据的变化,比如当页面离开、用户点击某个按钮后,就想要手动移除侦听。其实,watch 和 watchEffect 这两个 API 都会返回一个“停止侦听”的函数。
只要我们在适当的时机调用这个函数,当前的侦听器就会被移除,不再执行回调。例如:
watch 或 watchEffect 返回的停止函数。下面先看一个基本用法:
|<div id="app"> <input v-model="count" type="number"> <button @click="stopWatch">停止侦听</button> </div> <script> const { createApp, ref, watch } = Vue; const
在这个例子中,我们保存了watch返回的停止函数,当点击"停止侦听"按钮时,调用这个函数来停止侦听。
下面我们结合前面所学,包括 ref、computed、watch、watchEffect 等内容,通过一个详细的综合示例来实践。我们将实现一个具备以下功能的待办事项应用:
|<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>计算属性与侦听器示例</title> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
在这一部分中,我们深入学习了计算属性和侦听器。计算属性用于基于响应式数据计算派生值,具有缓存特性,适合处理复杂计算。侦听器用于在数据变化时执行副作用操作,可以精确控制要侦听的数据。 计算属性和侦听器是Vue中处理数据逻辑的重要工具。掌握它们后,你就能写出更加高效和可维护的代码。
在下一个部分,我们将学习组件,了解如何创建和使用组件,以及如何使用Props传递数据。组件是Vue应用的基本构建块,如果你想要构建一个模块化的应用,那么组件是必不可少的。