MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Composition API watch与computed的监听与计算属性实现

2023-08-146.1k 阅读

Vue Composition API 之 watch 与 computed 深入解析

在 Vue 的开发中,watchcomputed 是两个非常重要的特性,它们在数据处理和响应式系统中扮演着关键角色。在 Vue Composition API 中,这两个特性依然存在,并且以一种新的方式进行使用,为开发者提供了更灵活和强大的数据处理能力。

watch 的使用与原理

watch 主要用于监听数据的变化,并在数据变化时执行相应的操作。在 Vue Composition API 中,watch 的使用方式与传统的 Vue 选项式 API 略有不同,但核心功能保持一致。

  1. 基本使用 在 Vue Composition API 中,watch 函数接收两个参数:要监听的数据源和回调函数。例如,我们有一个响应式数据 count,我们可以这样监听它的变化:
import { ref, watch } from 'vue';

export default {
  setup() {
    const count = ref(0);

    watch(count, (newValue, oldValue) => {
      console.log(`Count has changed from ${oldValue} to ${newValue}`);
    });

    return {
      count
    };
  }
};

在上述代码中,watch(count, callback) 表示监听 count 的变化,当 count 的值发生改变时,会执行回调函数 callback,该回调函数接收两个参数:新值 newValue 和旧值 oldValue

  1. 监听复杂数据结构 watch 不仅可以监听简单的 ref 数据,还可以监听复杂的数据结构,如对象和数组。例如,我们有一个包含多个属性的对象:
import { reactive, watch } from 'vue';

export default {
  setup() {
    const user = reactive({
      name: 'John',
      age: 30
    });

    watch(user, (newValue, oldValue) => {
      console.log('User data has changed');
    }, { deep: true });

    return {
      user
    };
  }
};

在这个例子中,我们监听了 user 对象的变化。注意,由于对象是引用类型,直接监听对象时,只有当对象的引用发生变化时才会触发回调。如果我们想要监听对象内部属性的变化,需要设置 deep: true 选项。这样,当 user.nameuser.age 发生变化时,都会触发回调函数。

  1. 立即执行回调 有时候,我们希望在组件初始化时就执行一次监听回调,而不仅仅是在数据变化时执行。这可以通过设置 immediate: true 选项来实现:
import { ref, watch } from 'vue';

export default {
  setup() {
    const count = ref(0);

    watch(count, (newValue, oldValue) => {
      console.log(`Count has changed from ${oldValue} to ${newValue}`);
    }, { immediate: true });

    return {
      count
    };
  }
};

在上述代码中,immediate: true 使得回调函数在 count 初始化时就会执行一次,之后 count 发生变化时也会执行。

  1. 原理剖析 watch 的原理基于 Vue 的响应式系统。当我们使用 watch 监听一个数据源时,Vue 会在内部创建一个 Watcher 对象。这个 Watcher 对象会依赖于数据源的 getter 函数。当数据源的值发生变化时,会触发 setter 函数,进而通知所有依赖该数据源的 Watcher 对象,Watcher 对象会执行我们定义的回调函数。对于复杂数据结构的深度监听,Vue 会递归遍历对象或数组,为每个属性创建依赖,这就是 deep 选项的实现原理。

computed 的使用与原理

computed 用于创建计算属性,它的值会基于其他响应式数据进行计算,并且只有在其依赖的数据发生变化时才会重新计算。

  1. 基本使用 在 Vue Composition API 中,使用 computed 非常简单。例如,我们有两个 ref 数据 ab,我们想要创建一个计算属性 sum,它的值是 ab 的和:
import { ref, computed } from 'vue';

export default {
  setup() {
    const a = ref(1);
    const b = ref(2);

    const sum = computed(() => {
      return a.value + b.value;
    });

    return {
      a,
      b,
      sum
    };
  }
};

在上述代码中,computed 接收一个函数,该函数返回计算后的值。我们通过 sum.value 来获取计算属性的值。当 ab 的值发生变化时,sum 会重新计算。

  1. 计算属性的缓存 computed 最大的特点就是它的缓存机制。计算属性只有在它的依赖值发生变化时才会重新计算。例如,我们在模板中多次使用 sum 计算属性:
<template>
  <div>
    <p>a: {{ a }}</p>
    <p>b: {{ b }}</p>
    <p>Sum: {{ sum }}</p>
    <p>Sum again: {{ sum }}</p>
  </div>
</template>

<script>
import { ref, computed } from 'vue';

export default {
  setup() {
    const a = ref(1);
    const b = ref(2);

    const sum = computed(() => {
      console.log('Calculating sum...');
      return a.value + b.value;
    });

    return {
      a,
      b,
      sum
    };
  }
};
</script>

在上述代码中,当我们第一次访问 sum 时,会输出 Calculating sum...,表示计算属性进行了计算。但当我们再次访问 sum 时,并不会再次输出 Calculating sum...,因为 ab 的值没有发生变化,sum 使用了缓存的值。

  1. 可写的计算属性 通常情况下,计算属性是只读的。但在某些场景下,我们可能需要一个可写的计算属性。这可以通过为 computed 提供一个对象来实现,对象中包含 getset 函数:
import { ref, computed } from 'vue';

export default {
  setup() {
    const first = ref(1);
    const second = ref(2);

    const sum = computed({
      get: () => {
        return first.value + second.value;
      },
      set: (newValue) => {
        const diff = newValue - (first.value + second.value);
        first.value += diff / 2;
        second.value += diff / 2;
      }
    });

    return {
      first,
      second,
      sum
    };
  }
};

在上述代码中,我们定义了一个可写的计算属性 sum。当我们设置 sum.value 时,会触发 set 函数,在 set 函数中,我们根据新值和旧值的差异来更新 firstsecond 的值。

  1. 原理剖析 computed 的原理同样基于 Vue 的响应式系统。当我们创建一个计算属性时,Vue 会创建一个 ComputedWatcher 对象。这个 ComputedWatcher 对象会依赖于计算属性函数中使用的响应式数据的 getter 函数。当依赖的数据发生变化时,会标记计算属性为脏,下次访问计算属性时会重新计算值。计算属性的缓存是通过一个标志位来实现的,当计算属性的值没有变化时,会直接返回缓存的值,而不会重新计算。

watch 与 computed 的应用场景对比

  1. watch 的应用场景

    • 异步操作:当数据变化需要执行异步操作时,watch 是一个很好的选择。例如,当用户输入搜索关键词时,我们需要根据关键词进行异步搜索并更新搜索结果。
    import { ref, watch } from 'vue';
    
    export default {
      setup() {
        const searchQuery = ref('');
        const searchResults = ref([]);
    
        watch(searchQuery, async (newValue) => {
          if (newValue) {
            const response = await fetch(`/api/search?query=${newValue}`);
            const data = await response.json();
            searchResults.value = data;
          } else {
            searchResults.value = [];
          }
        });
    
        return {
          searchQuery,
          searchResults
        };
      }
    };
    
    • 监听复杂数据变化:如前文所述,当需要监听对象或数组内部属性的变化时,watch 配合 deep 选项可以很好地满足需求。例如,在一个购物车应用中,监听购物车中商品列表的变化,包括商品数量、价格等属性的改变,以便实时更新总价等信息。
  2. computed 的应用场景

    • 数据计算与缓存:当需要基于其他响应式数据进行计算,并且希望缓存计算结果以提高性能时,computed 是首选。例如,在一个电商应用中,计算购物车中商品的总价,由于总价是基于商品列表中的每个商品的价格和数量计算得出,使用 computed 可以在商品信息变化时自动更新总价,并且在商品信息未变化时使用缓存的值,避免重复计算。
    • 模板中复杂表达式简化:在模板中,如果有复杂的表达式,使用 computed 可以将其提取出来,使模板更加简洁易读。例如,在一个博客应用中,文章的阅读量、点赞数、评论数等数据需要进行一些复杂的计算和格式化后在模板中展示,将这些计算逻辑封装在 computed 中,可以使模板代码更清晰。

高级应用与注意事项

  1. 同时监听多个数据源 在某些情况下,我们可能需要同时监听多个数据源的变化。在 Vue Composition API 中,可以通过传递数组作为 watch 的第一个参数来实现:
import { ref, watch } from 'vue';

export default {
  setup() {
    const a = ref(1);
    const b = ref(2);

    watch([a, b], (newValues, oldValues) => {
      console.log(`a has changed to ${newValues[0]}, b has changed to ${newValues[1]}`);
    });

    return {
      a,
      b
    };
  }
};

在上述代码中,watch([a, b], callback) 表示同时监听 ab 的变化。当 ab 其中任何一个发生变化时,都会触发回调函数,newValuesoldValues 是包含 ab 新旧值的数组。

  1. 在 watch 中使用异步操作的注意事项 当在 watch 的回调函数中使用异步操作时,需要注意回调函数执行的时机。由于异步操作是异步执行的,可能会导致在数据变化后,异步操作还未完成时又发生了新的数据变化。为了解决这个问题,可以使用防抖(debounce)或节流(throttle)技术。例如,使用防抖函数可以确保在一定时间内多次触发数据变化时,只执行一次异步操作:
import { ref, watch } from 'vue';

function debounce(func, delay) {
  let timer;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

export default {
  setup() {
    const searchQuery = ref('');
    const searchResults = ref([]);

    const debouncedSearch = debounce(async (newValue) => {
      if (newValue) {
        const response = await fetch(`/api/search?query=${newValue}`);
        const data = await response.json();
        searchResults.value = data;
      } else {
        searchResults.value = [];
      }
    }, 300);

    watch(searchQuery, debouncedSearch);

    return {
      searchQuery,
      searchResults
    };
  }
};

在上述代码中,我们定义了一个 debounce 函数,并将其应用在 watch 的回调函数中。这样,当 searchQuery 变化时,只有在停止变化 300 毫秒后才会执行异步搜索操作,避免了频繁触发搜索请求。

  1. computed 中的依赖优化 在定义 computed 时,要确保计算属性函数中只依赖真正需要的数据。如果依赖了不必要的数据,可能会导致计算属性在不需要重新计算时也进行重新计算,影响性能。例如,在一个用户信息展示组件中,计算属性 fullName 只依赖于 firstNamelastName,而不应该依赖于用户的年龄等无关数据。
import { ref, computed } from 'vue';

export default {
  setup() {
    const firstName = ref('John');
    const lastName = ref('Doe');
    const age = ref(30);

    const fullName = computed(() => {
      return firstName.value + ' ' + lastName.value;
    });

    return {
      firstName,
      lastName,
      age,
      fullName
    };
  }
};

在上述代码中,fullName 的计算只依赖于 firstNamelastName,即使 age 发生变化,fullName 也不会重新计算,从而提高了性能。

总结与实践建议

watchcomputed 是 Vue Composition API 中非常强大的特性,它们在数据处理和响应式系统中发挥着重要作用。在实际开发中,要根据具体的业务需求合理选择使用 watch 还是 computed。对于需要监听数据变化并执行异步操作或复杂逻辑的场景,优先考虑 watch;对于需要基于其他响应式数据进行计算并缓存结果的场景,computed 是更好的选择。同时,在使用过程中要注意优化性能,如在 watch 中合理使用防抖、节流技术,在 computed 中确保依赖的准确性。通过深入理解和熟练运用 watchcomputed,可以开发出更高效、更灵活的 Vue 应用程序。在实际项目中,多进行实践和总结,不断优化代码结构和性能,以提升用户体验和开发效率。