分类课程智能体AI
文章
订阅
分类课程AI导师
文章
价格
课程进度
7 / 15
上一节表单处理下一节组件
自在学

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

公网安备湘公网安备43020302000292号 | 湘ICP备2025148919号-1

关于我们隐私政策使用条款

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

公网安备湘公网安备43020302000292号湘ICP备2025148919号-1

编程Vue指南计算属性与侦听器

计算属性与侦听器

虽然在前面的课程中我们学习了如何在模板中使用表达式,但在某些情况下,模板中的表达式可能会变得复杂,或者我们需要在多个地方使用相同的逻辑。Vue提供了计算属性(computed)和侦听器(watch)来处理这些场景。

计算属性用于基于响应式数据计算派生值,而侦听器用于在数据变化时执行副作用操作。在这一堂课中,我们将学习它们的用法,了解它们之间的区别,以及如何在实际项目中正确使用它们。

计算属性与侦听器


为什么需要计算属性

让我们先看一个例子,理解为什么需要计算属性。假设我们有一个购物车,需要显示商品的总价:

html
<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没有变化。 使用计算属性可以解决这些问题:

html
<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;
  
  const app = createApp({
    setup() {
      const cart = ref([
        { id: 1, name: '商品1', price: 100, quantity: 2 },
        { id: 2, name: '商品2', price: 200, quantity: 1 }
      ]);
      
      const totalPrice = computed(() => {
        return cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0);
      });
      
      return {
        cart,
        totalPrice
      };
    }
  });
  
  app.mount('#app');
</script>

现在,总价的计算逻辑被封装在totalPrice计算属性中。代码更清晰,而且计算属性会缓存结果,只有当依赖的数据(cart)发生变化时才会重新计算。

计算属性的基本用法

在组合式API中,我们使用computed函数来创建计算属性。computed接收一个函数,这个函数返回计算后的值:

html
<div id="app">
  <p>名字:{{ firstName }}</p>
  <p>姓氏:{{ lastName }}</p>
  <p>全名:{{ fullName }}</p>
</div>
 
<script>
  const { createApp, ref, computed } = Vue;
  
  const app = createApp({
    setup() {
      const firstName = ref('张');
      const lastName = ref('三');
      
      const fullName = computed(() => {
        return firstName.value + lastName.value;
      });
      
      return {
        firstName,
        lastName,
        fullName
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,fullName是一个计算属性,它基于firstName和lastName计算全名。当firstName或lastName改变时,fullName会自动更新。

计算属性是响应式的,我们可以在模板中像使用普通数据一样使用它。计算属性也是缓存的,只有当它的依赖发生变化时才会重新计算。

计算属性 vs 方法

你可能会问,为什么不用方法来实现同样的功能呢?其实,计算属性(computed)和方法(methods)虽然都能实现类似的动态计算,但它们在行为和性能上有重要区别:

  • 计算属性的结果会基于它的依赖进行缓存,只有当依赖变化时才会重新计算,适合依赖性强、可能被多次使用的场景。
  • 方法则不会缓存,每次调用都会重新执行函数,无论依赖的数据有没有发生变化。

下面我们通过代码示例,对比计算属性和方法的用法和效果:

html
<div id="app">
  <p>计算属性:{{ reversedMessage }}</p>
  <p>计算属性:{{ reversedMessage }}</p>
  <p>方法:{{ reverseMessage() }}</p>
  <p>方法:{{ reverseMessage() }}</p>
</div>
 
<script>
  const { createApp, ref, computed } = Vue;
  
  const app = createApp({
    setup() {
      const message = ref('Hello Vue');
      
      const reversedMessage = computed(() => {
        console.log('计算属性被调用');
        return message.value.split('').reverse().join('');
      });
      
      const reverseMessage = () => {
        console.log('方法被调用');
        return message.value.split('').reverse().join('');
      };
      
      return {
        message,
        reversedMessage,
        reverseMessage
      };
    }
  });
  
  app.mount('#app');
</script>

如果你运行这段代码,你会看到"计算属性被调用"只打印一次,而"方法被调用"会打印两次。这是因为计算属性会缓存结果,只有当依赖的数据变化时才会重新计算;而方法每次调用都会执行。

如果计算逻辑很简单,或者需要传递参数,使用方法更合适。如果计算逻辑复杂,或者需要在多个地方使用,使用计算属性更合适。

计算属性的getter和setter

默认情况下,计算属性是只读的——你只能读取它们的值,不能直接修改它们。但如果我们想让计算属性既能读取也能设置(即“可写”),就需要为它定义 getter(读取函数)和 setter(设置函数)。 getter 决定了我们在访问计算属性时返回什么结果,setter 决定了在给计算属性赋值时该如何处理这个值。这样,计算属性就像是某个数据的“复合视图”,它的读取和修改都可以自定义逻辑。下面我们来看具体怎么实现:

html
<div id="app">
  <p>名字:{{ firstName }}</p>
  <p>姓氏:{{ lastName }}</p>
  <p>全名:{{ fullName }}</p>
  <input v-model="fullName" placeholder="修改全名">
</div>
 
<script>
  const { createApp, ref, computed } = Vue;
  
  const app = createApp({
    setup() {
      const firstName = ref('张');
      const lastName = ref('三');
      
      const fullName = computed({
        get() {
          return firstName.value + lastName.value;
        },
        set(newValue) {
          const names = newValue.split('');
          firstName.value = names[0] || '';
          lastName.value = names.slice(1).join('') || '';
        }
      });
      
      return {
        firstName,
        lastName,
        fullName
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,我们为fullName提供了getter和setter。getter返回全名,setter根据新值更新firstName和lastName。现在,我们可以通过v-model直接绑定到fullName,修改全名会自动更新名字和姓氏。


使用watch侦听数据变化

在实际开发中,很多场景下我们需要在某个数据发生变化时“做点事情”,比如:用户输入后自动进行搜索、表单值更新时校验、某个状态变化时去请求新数据、或者某些变量变动时手动更新页面中的某个部分。
此时,watch 函数就非常有用。它可以用来“侦听”一个或多个响应式数据的变化——每当这些数据发生改变时,会自动执行我们指定的回调函数。
这样的机制可以让你灵活地插入副作用操作:比如调用接口、操作 DOM、节流防抖、写日志等等。通过 watch,你可以把“某个数据变化时需要联动的逻辑”与其它业务代码很好地解耦,让你的逻辑更清晰、更易维护。

让我们看一个简单的例子:

html
<div id="app">
  <input v-model="searchQuery" placeholder="搜索">
  <p>搜索关键词:{{ searchQuery }}</p>
</div>
 
<script>
  const { createApp, ref, watch } = Vue;
  
  const app = createApp({
    setup() {
      const searchQuery = ref('');
      
      watch(searchQuery, (newValue, oldValue) => {
        console.log('搜索关键词变化:', oldValue, '->', newValue);
        // 这里可以执行搜索操作
      });
      
      return {
        searchQuery
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,我们使用watch来侦听searchQuery的变化。当searchQuery的值改变时,回调函数会被调用,我们可以在这里执行搜索操作。

watch的第一个参数是要侦听的数据,第二个参数是回调函数。回调函数接收两个参数:新值和旧值。

侦听多个数据源

我们也可以同时侦听多个数据源,当任何一个数据源发生变化时,回调函数都会被调用。

html
<div id="app">
  <input v-model="firstName" placeholder="名字">
  <input v-model="lastName" placeholder="姓氏">
  <p>全名:{{ firstName }} {{ lastName }}</p>
</div>
 
<script>
  const { createApp, ref, watch } = Vue;
  
  const app = createApp({
    setup() {
      const firstName = ref('张');
      const lastName = ref('三');
      
      watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
        console.log('名字变化:', oldFirst, '->', newFirst);
        console.log('姓氏变化:', oldLast, '->', newLast);
      });
      
      return {
        firstName,
        lastName
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,我们使用数组来同时侦听firstName和lastName。回调函数接收两个数组:新值数组和旧值数组。

深度侦听

默认情况下,watch 只会侦听被监视数据的最外层(顶层)的变化,也就是说,仅当对这个对象或数组的引用本身发生改变时才会触发回调,而不会对对象中的嵌套属性或数组内部的元素进行追踪。例如,如果你对对象的某个属性赋新值,或者向数组里添加/删除元素,watch 默认是感知不到这些深层次变化的。

如果我们要监听对象或数组内部属性的任何变化(比如对象的属性被修改、数组内容被修改),这时就需要启用“深度侦听”功能。具体做法是给 watch 的第三个参数传递 { deep: true },这样无论对象哪一层属性或数组里的哪一项发生变化,回调都会被调用。

下面我们看一个相关的例子:

html
<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, watch } = Vue;
  
  const app = createApp({
    setup() {
      const user = reactive({
        name: '张三',
        age: 25
      });
      
      watch(user, (newValue, oldValue) => {
        console.log('用户信息变化:', newValue);
      }, { deep: true });
      
      return {
        user
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,我们使用{ deep: true }选项来启用深度侦听。现在,当user.name或user.age改变时,watch都会触发。

注意,对于reactive对象,深度侦听是默认启用的。但对于ref对象,我们需要显式启用:

html
<script>
  const { createApp, ref, watch } = Vue;
  
  const app = createApp({
    setup() {
      const user = ref({
        name: '张三',
        age: 25
      });
      
      watch(user, (newValue, oldValue) => {
        console.log('用户信息变化:', newValue);
      }, { deep: true });
      
      return {
        user
      };
    }
  });
</script>

立即执行

默认情况下,watch只在数据变化时执行。如果我们希望在初始化时也执行一次,可以使用{ immediate: true }选项:

html
<div id="app">
  <input v-model="searchQuery" placeholder="搜索">
</div>
 
<script>
  const { createApp, ref, watch } = Vue;
  
  const app = createApp({
    setup() {
      const searchQuery = ref('Vue');
      
      watch(searchQuery, (newValue) => {
        console.log('执行搜索:', newValue);
        // 执行搜索操作
      }, { immediate: true });
      
      return {
        searchQuery
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,watch会在组件初始化时立即执行一次,然后每次searchQuery变化时也会执行。


watchEffect

watchEffect 是 Vue 3 新增的一个响应式侦听 API。它的作用是:你只需编写一个回调函数,在这个函数内部用到的所有响应式数据(ref、reactive 等)都会被 Vue 自动追踪,一旦这些数据发生变化,回调就会重新执行。 也就是说,watchEffect 不需要你手动指定侦听的对象,它会自动收集依赖。

每当你在 watchEffect 的回调内访问某个响应式属性,Vue 就会把这些属性关联到这个回调。当这些响应式数据变化时,watchEffect 注册的回调会立即再次运行。 并且,watchEffect 在初始化时会立即执行一次回调,这和 watch 的 { immediate: true } 效果类似。

这个 API 特别适合需要根据多项响应式数据自动计算、同步副作用或执行逻辑的场景。比如自动做网络请求、同步派生状态等操作都很方便,相比 watch,使用更加简洁。

html
<div id="app">
  <input v-model="count" type="number">
  <p>计数:{{ count }}</p>
  <p>双倍计数:{{ doubleCount }}</p>
</div>
 
<script>
  const { createApp, ref, watchEffect } = Vue;
  
  const app = createApp({
    setup() {
      const count = ref(0);
      const doubleCount = ref(0);
      
      watchEffect(() => {
        doubleCount.value = count.value * 2;
      });
      
      return {
        count,
        doubleCount
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,watchEffect会自动追踪count,当count变化时,回调函数会重新执行,更新doubleCount。

watchEffect和watch的区别在于,watchEffect不需要显式指定要侦听的数据,它会自动追踪回调函数中使用的所有响应式数据。watchEffect也会在初始化时立即执行一次。


watch vs watchEffect

watch和watchEffect各有优缺点。watch更明确,可以精确控制要侦听的数据;watchEffect更简洁,会自动追踪依赖。

一般来说,如果你需要侦听特定的数据,使用watch;如果你需要自动追踪依赖,使用watchEffect。

让我们看一个使用watchEffect的例子:

html
<div id="app">
  <input v-model="searchQuery" placeholder="搜索">
  <div v-if="loading">加载中...</div>
  <div v-else>搜索结果:{{ results }}</div>
</div>
 
<script>
  const { createApp, ref, watchEffect } = Vue;
  
  const app = createApp({
    setup() {
      const searchQuery = ref('');
      const loading = ref(false);
      const results = ref([]);
      
      watchEffect(async () => {
        if (searchQuery.value) {
          loading.value = true;
          // 模拟搜索请求
          await new Promise(resolve => setTimeout(resolve, 500));
          results.value = [`${searchQuery.value}的结果1`, `${searchQuery.value}的结果2`];
          loading.value = false;
        } else {
          results.value = [];
        }
      });
      
      return {
        searchQuery,
        loading,
        results
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,watchEffect会自动追踪searchQuery,当它变化时,会自动执行搜索操作。


停止侦听

有时候,在某些场景下我们不希望一直侦听某个数据的变化,比如当页面离开、用户点击某个按钮后,就想要手动移除侦听。其实,watch 和 watchEffect 这两个 API 都会返回一个“停止侦听”的函数。 只要我们在适当的时机调用这个函数,当前的侦听器就会被移除,不再执行回调。例如:

  1. 用变量保存 watch 或 watchEffect 返回的停止函数。
  2. 在需要的时候(比如事件回调、生命周期钩子等)调用这个停止函数。

下面先看一个基本用法:

html
<div id="app">
  <input v-model="count" type="number">
  <button @click="stopWatch">停止侦听</button>
</div>
 
<script>
  const { createApp, ref, watch } = Vue;
  
  const app = createApp({
    setup() {
      const count = ref(0);
      
      const stopWatch = watch(count, (newValue) => {
        console.log('计数变化:', newValue);
      });
      
      const stop = () => {
        stopWatch();
        alert('已停止侦听');
      };
      
      return {
        count,
        stopWatch: stop
      };
    }
  });
  
  app.mount('#app');
</script>

在这个例子中,我们保存了watch返回的停止函数,当点击"停止侦听"按钮时,调用这个函数来停止侦听。


综合示例

下面我们结合前面所学,包括 ref、computed、watch、watchEffect 等内容,通过一个详细的综合示例来实践。我们将实现一个具备以下功能的待办事项应用:

  • 能够添加、删除待办事项;
  • 支持勾选完成,自动统计已完成和未完成数量;
  • 通过按钮切换全部、未完成、已完成事项的过滤视图(使用计算属性);
  • 利用侦听器自动保存 todos 到本地存储,并在页面加载时自动读取恢复(体现 watch 的用法);
  • 每次列表变更时通过 watchEffect 输出提示信息。
html
<!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>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 600px;
      margin: 50px auto;
      padding: 20px;
    }
    input[type="text"] {
      width: 100%;
      padding: 10px;
      margin-bottom: 20px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    button {
      padding: 5px 10px;
      margin-right: 5px;
      background-color: #42b983;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    ul {
      list-style: none;
      padding: 0;
    }
    li {
      padding: 10px;
      margin: 5px 0;
      background-color: #f5f5f5;
      border-radius: 4px;
      display: flex;
      align-items: center;
      gap: 10px;
    }
    .completed {
      text-decoration: line-through;
      color: #999;
    }
  </style>
</head>
<body>
  <div id="app">
    <h2>待办事项</h2>
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="输入待办事项">
    
    <div>
      <button @click="filter = 'all'">全部</button>
      <button @click="filter = 'active'">未完成</button>
      <button @click="filter = 'completed'">已完成</button>
    </div>
    
    <ul>
      <li v-for="todo in filteredTodos" :key="todo.id">
        <input type="checkbox" v-model="todo.completed">
        <span :class="{ completed: todo.completed }">{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">删除</button>
      </li>
    </ul>
    
    <p>总计:{{ totalTodos }} 项,已完成:{{ completedTodos }} 项,未完成:{{ activeTodos }} 项</p>
  </div>
  
  <script>
    const { createApp, ref, computed, watch } = Vue;
    
    const app = createApp({
      setup() {
        const newTodo = ref('');
        const todos = ref([
          { id: 1, text: '学习Vue.js', completed: false },
          { id: 2, text: '完成项目', completed: true }
        ]);
        const filter = ref('all');
        
        let nextId = 3;
        
        const addTodo = () => {
          if (newTodo.value.trim()) {
            todos.value.push({
              id: nextId++,
              text: newTodo.value,
              completed: false
            });
            newTodo.value = '';
          }
        };
        
        const removeTodo = (id) => {
          const index = todos.value.findIndex(todo => todo.id === id);
          if (index > -1) {
            todos.value.splice(index, 1);
          }
        };
        
        const totalTodos = computed(() => todos.value.length);
        const completedTodos = computed(() => {
          return todos.value.filter(todo => todo.completed).length;
        });
        const activeTodos = computed(() => {
          return todos.value.filter(todo => !todo.completed).length;
        });
        const filteredTodos = computed(() => {
          if (filter.value === 'active') {
            return todos.value.filter(todo => !todo.completed);
          } else if (filter.value === 'completed') {
            return todos.value.filter(todo => todo.completed);
          } else {
            return todos.value;
          }
        });
        
        watch(todos, (newTodos) => {
          console.log('待办事项变化,当前数量:', newTodos.length);
        }, { deep: true });
        
        return {
          newTodo,
          todos,
          filter,
          addTodo,
          removeTodo,
          totalTodos,
          completedTodos,
          activeTodos,
          filteredTodos
        };
      }
    });
    
    app.mount('#app');
  </script>
</body>
</html>

下一步

在这一部分中,我们深入学习了计算属性和侦听器。计算属性用于基于响应式数据计算派生值,具有缓存特性,适合处理复杂计算。侦听器用于在数据变化时执行副作用操作,可以精确控制要侦听的数据。 计算属性和侦听器是Vue中处理数据逻辑的重要工具。掌握它们后,你就能写出更加高效和可维护的代码。

在下一个部分,我们将学习组件,了解如何创建和使用组件,以及如何使用Props传递数据。组件是Vue应用的基本构建块,如果你想要构建一个模块化的应用,那么组件是必不可少的。

  • 为什么需要计算属性
    • 计算属性的基本用法
    • 计算属性 vs 方法
    • 计算属性的getter和setter
  • 使用watch侦听数据变化
    • 侦听多个数据源
    • 深度侦听
    • 立即执行
  • watchEffect
  • watch vs watchEffect
  • 停止侦听
  • 综合示例
  • 下一步

目录

  • 为什么需要计算属性
    • 计算属性的基本用法
    • 计算属性 vs 方法
    • 计算属性的getter和setter
  • 使用watch侦听数据变化
    • 侦听多个数据源
    • 深度侦听
    • 立即执行
  • watchEffect
  • watch vs watchEffect
  • 停止侦听
  • 综合示例
  • 下一步