深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)

#五一技术创作马拉松#

深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)_第1张图片

前言

计算属性 computed 和侦听器 watch 都是 Vue.js 框架中用来响应式更新视图的重要概念。在 Vue 项目开发中,这两个技术点是非常重要的,同时也是 Vue 基础中不可缺少的知识点。在面试中,计算属性 computed 和侦听器 watch也是经常出现的考点,作为前端开发也是必须要掌握的,这篇文章详细看看如何使用计算属性 computed 和侦听器 watch ,以及谈谈之前遇到的关于计算属性 computed 和侦听器 watch 的面试题。

相关文章:浅谈在 Vue2 和 Vue3 中计算属性和侦听器的一些变化
官方介绍:计算属性 | Vue.js、侦听器 | Vue.js


文章目录

  • 前言
  • 计算属性
    • 计算属性的定义
    • 计算属性的用法
    • 计算属性的缓存
  • 侦听器
    • 侦听器性的定义
    • 侦听器的用法
    • watchEffect() 函数
    • watch 与 watchEffect
    • 停止侦听器
  • 二者区别
  • 最后


计算属性

计算属性的定义

Vue.js 的插值在 HTML 模板中是支持 JavaScript 表达式的。但是如果当表达式过于复杂,模板代码就会变得非常臃肿且可读性差,难以维护。比如说,反转字符串这段代码。

<p>{{message.splice('').reverse().join('') }}</p>

这个表达式首先要将 message 变量按照每一个字母分解成一个字符串数组,然后将字符串数组翻转,接着再拼接字符串数组里面的内容成一个新的字符串,最终达到字符串翻转的效果。这里一个表达式就含有三个 JavaScript 方法,逻辑过于复杂,可读性非常差。

如果页面中有多个地方需要这个字符串翻转的表达式,那整体的 Vue 模板的代码将会变得非常难以维护,而且在 MVVM 模式下,模板中过多的处理数据计算,会导致模板和控制器之间高度耦合。这种情况下,就需要使用计算属性了。

计算属性的作用就是为了解决表达式逻辑过于复杂的场景,使页面尽可能没有数据复杂处理。计算属性是以函数的形式出现在组件的 computed 选项中,在新版的 Vue 3.x 的 setup 语法糖里,可以将计算属性的具体实现函数传递给 computed() 方法,然后赋值给一个变量。如果将上述翻转字符串的表达式通过计算属性实现,代码如下。

<template>
  <p>原始text:{{ message }}p>
  <p>计算属性text:{{ reverseMessage }}p>
template>

<script setup>
import { computed, ref } from "vue";
const message = ref("hello world");
const reverseMessage = computed(() => {
  return message.value.split("").reverse().join("");
});
script>

深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)_第2张图片
这里的 reversedMessage 就是计算属性,它能够像其他变量一样,通过 Mustache 语法作为插值绑定到模板中。而且如果 message 变量被修改,对应的 reversedMessage 的结果也会立刻被修改。

计算属性的用法

在 Vue 3.x 新版的 setup 语法中,计算属性有两种用法。第一种如上述的代码例子,直接将实现函数以参数的形式,然后传递给 computed() 方法中实现。另一种则是分别具体实现 getter 方法和 setter 方法,然后再封装成对象传递给 computed() 方法。

我们看下面这段代码,商品的单价在组件的 data 中 price 表示,通过计算属性,来看看买多个商品要多少钱。

<template>
  <p>单价:{{ price }} 元/件p>
  <p>总价:{{ totalPrice }} 元p>
template>

<script setup>
import { computed, ref } from "vue";
const price = ref(6);
const totalPrice = computed({
  get: () => {
    return price.value * 5;
  },
  set: (value) => {
    price.value = value / 5;
  },
});
script>

这里在 computed() 方法中接收了含义 get 变量和 set 变量的对象,其中 get 变量对应 getter 方法实现,set 则是对应 setter 方法实现。
深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)_第3张图片
在浏览器中,可以使用 vue devtools 这个插件来手动修改 totalPrice 的值,然后可以看到页面的单价和总价都发生了变化。
深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)_第4张图片
这里的数字变化是因为在代码中实现了 tolalPrice 计算属性的 selter 方法,当用户修改 totaiPrice 的数值,totalPricesetter 方法就会被调用,因而在 setter 内部被修改的 price 数值也会发生变化。但是此时整个调用链还没有结束,因为 price 的数值发生变动,触发了响应式机制,所以 totalPrice 的数值会根据最新的 price 数值重新计算,渲染最后的计算结果显示在页面上。从而达到单价和总价两个数字同时发生改变

这里有两个细节点需要注意:第一,如果计算属性没有实现 setter 方法,那么在 vue devtools 里计算属性变量是不可以被修改的,第二,如果被计算属性监测的变量在计算属性的 setter 方法内被修改,那么计算属性最终的数值会以被监测变量变化之后的最新数值重新计算或渲染。

计算属性的缓存

计算属性的缓存是指,当计算属性依赖的数据发生变化时,计算属性会重新计算并缓存新的值。在缓存有效期内,如果再次访问该计算属性,则直接返回缓存中的值,不需要重新计算。

❗注意:如果计算属性函数中的数据不是响应式依赖,就不会触发计算属性的缓存机制。比如 Date().now()Date().getTime() 都不是响应式依赖,因此它们的计算属性不会更新

这里我们可以看一个很经典的对比,对比看计算属性的缓存和方法,看看页面渲染的效果。我们以下面这段代码作为案例。

<template>
  <p>单价:{{ price }} 元/件p>
  <p>computed总价:{{ totalPrice }} 元p>
  <p>computed总价:{{ totalPrice }} 元p>
  <p>computed总价:{{ totalPrice }} 元p>
  <p>methods总价:{{ methods() }} 元p>
  <p>methods总价:{{ methods() }} 元p>
  <p>methods总价:{{ methods() }} 元p>
template>

<script setup>
import { computed, ref, watch, watchEffect } from "vue";
const price = ref(6);
const totalPrice = computed(() => {
  console.log("调用computed()");
  return price.value * 5;
});
const methods = () => {
  console.log("调用methods()");
  return price.value * 5;
};
script>

深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)_第5张图片
如上图,我们可以看到页面渲染效果和控制台打印的信息,计算属性的函数只调用了一次,而普通的方法函数则调用了多次。原因就是计算机属性将基于它的响应式依赖关系缓存。即便是多次使用只需要计算一次,而方法则是用多少次就会被执行多少次。

这种缓存机制可以有效地提高应用程序的性能。例如,如果计算属性需要遍历一个大型数组并进行复杂的运算,而且这个数组的数据变化频繁,如果每次都重新计算,会极大地降低应用程序的响应速度。但是,如果使用了计算属性的缓存机制,只有数组数据发生变化时才会重新计算,大大节约了计算资源和时间。

需要注意的是,如果计算属性依赖的数据未被定义为响应式的,或者计算属性的 getter 函数中包含了异步操作或者时间较长的同步操作,那么计算属性的缓存可能会失效或者带来其他副作用。因此,需要根据实际情况谨慎使用计算属性的缓存机制,或者使用其他方式来解决以上问题。面对这种情况,我们换一种思路来使用侦听器。


侦听器

侦听器性的定义

通常情况下,我们可以使用计算属性来处理数据的计算和衍生,但是在某些情况下,计算属性并不够灵活,不能满足我们的需求。例如,需要在数据变化时执行异步操作、发送网络请求或者操作 DOM 等。这个时候,我们可以使用侦听器来处理这些需求。

在 Vue.js 中,侦听器是一个函数,它可以监听响应式数据的变化,当数据发生变化时,侦听器会被触发执行。下面是一个使用侦听器的例子。

<template>
  <input type="number" v-model="num" />
  <p>result为:{{ result }}p>
template>

<script setup>
import { ref, watch } from "vue";
const num = ref(0);
const result = ref(0);
watch(
  () => num.value,
  (newValue, oldValue) => {
    console.log("旧值为:", oldValue, "新值为:", newValue);
    return (result.value = num.value * num.value);
  }
);
script>

深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)_第6张图片
上面的代码,我们通过输入框输入数字修改 num 值,侦听器通过监听 num 值的变化,计算输入数字的结果。这个功能与计算属性很类似,但区别是侦听器可以检测数值的变化,以及在控制台的输出。

侦听器其实是计算属性的底层逻辑实现。不同之处在于侦听器可以知道变化前后的数据数值以及在侦听器之内可以使用异步方法,在侦听器里添加异步操作是一个很重要的操作,这里可以是网络请求,也可以是开销较大的计算,这是计算属性中没有的。

侦听器的用法

在 Vue 3.x 的 setup 语法中,侦听器的语法如下。

watch{
	被监测属性,
	监测到变化时的回调函数,
	参数选项
}

其中,第二个监测到变化时的回调函数的可传参数如下。

() => {
	// 无参数
}
(value) => {
	// 变化后的数值作为参数传入
}
(newValue, oldValue) => {
	// 变化前的数值和变化后的数值作为参数传入
}

所以侦听器不仅可以监测基本数据类型的 ref() 数据,同时还可以监测引用数据类型的 reactive() 数据。不过监测引用数据类型的数据,需要将 { deep: true } 属性作为第三个参数传给 watch() 函数。

{ deep: true } 选项可以告诉 Vue 在监测数据变化时,递归到深层嵌套的属性值中去。这样即使属性值是引用类型,只要其内部的属性值发生了变化,watch 回调函数也能够正常触发执行。看下面这段代码。

<template>
  <div>
    <p>user: {{ data.user }}p>
    <button @click="updateUser">Update Userbutton>
  div>
template>

<script setup>
import { reactive, watch } from "vue";

const data = reactive({
  user: {
    name: "张三",
    age: 20,
    hobby: ["唱歌", "跳舞"],
  },
});

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

function updateUser() {
  data.user.name = "李四";
  data.user.hobby.push("游泳");
}
script>

深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)_第7张图片
在这个例子中,我们创建了一个包含嵌套属性的 reactive()数据对象,并使用 watch 监测了 user 属性。在 watch 选项中,我们使用 { deep: true } 选项来告诉 Vue 在监测数据变化时递归到嵌套属性中去。

当用户点击 Update User 按钮时,updateUser 函数会更新 user 属性中的 namehobby 属性值。这里注意,我们使用了 data.user.namedata.user.hobby.push() 的方式来更新属性值,而不是直接赋值一个新的对象。这样就能够确保 user 属性的引用地址不变,从而让 deep 选项生效。
深入与浅谈 Vue 中计算属性和侦听器的区别和使用(Vue3版本为例)_第8张图片
此时,由于我们监测了 user属性,并使用了 { deep: true } 选项,当 user 属性中的嵌套属性值发生变化时,watch 回调函数就会被触发执行,打印出相应的信息。

❗注意:深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

即时回调的侦听器​
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

watch(source, (newValue, oldValue) => {
  // 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

watchEffect() 函数

侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId 的引用发生变化时使用侦听器来加载一个远程资源:

const todoId = ref(1)
const data = ref(null)

watch(todoId, async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
}, { immediate: true })

特别是注意侦听器是如何两次使用 todoId 的,一次是作为源,另一次是在回调中。

我们可以用 watchEffect 函数 来简化上面的代码。watchEffect()允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。

对于这种只有一个依赖项的例子来说,watchEffect() 的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect() 可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。

✅TIP:watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。

watch 与 watchEffect

watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

停止侦听器

setup()

你可能感兴趣的:(前端,#,Vue,#,前端开发面试题和经验,前端,vue3,watch,computed,计算属性和侦听器)