#五一技术创作马拉松#
计算属性 computed
和侦听器 watch
都是 Vue.js 框架中用来响应式更新视图的重要概念。在 Vue 项目开发中,这两个技术点是非常重要的,同时也是 Vue 基础中不可缺少的知识点。在面试中,计算属性 computed
和侦听器 watch
也是经常出现的考点,作为前端开发也是必须要掌握的,这篇文章详细看看如何使用计算属性 computed
和侦听器 watch
,以及谈谈之前遇到的关于计算属性 computed
和侦听器 watch
的面试题。
相关文章:浅谈在 Vue2 和 Vue3 中计算属性和侦听器的一些变化
官方介绍:计算属性 | Vue.js、侦听器 | Vue.js
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>
这里的 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 devtools
这个插件来手动修改 totalPrice
的值,然后可以看到页面的单价和总价都发生了变化。
这里的数字变化是因为在代码中实现了 tolalPrice
计算属性的 selter
方法,当用户修改 totaiPrice
的数值,totalPrice
的 setter
方法就会被调用,因而在 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>
如上图,我们可以看到页面渲染效果和控制台打印的信息,计算属性的函数只调用了一次,而普通的方法函数则调用了多次
。原因就是计算机属性将基于它的响应式依赖关系缓存。即便是多次使用只需要计算一次,而方法则是用多少次就会被执行多少次。
这种缓存机制可以有效地提高应用程序的性能。例如,如果计算属性需要遍历一个大型数组并进行复杂的运算,而且这个数组的数据变化频繁,如果每次都重新计算,会极大地降低应用程序的响应速度。但是,如果使用了计算属性的缓存机制,只有数组数据发生变化时才会重新计算,大大节约了计算资源和时间。
需要注意的是,如果计算属性依赖的数据未被定义为响应式的,或者计算属性的 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>
上面的代码,我们通过输入框输入数字修改 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>
在这个例子中,我们创建了一个包含嵌套属性的 reactive()
数据对象,并使用 watch
监测了 user
属性。在 watch
选项中,我们使用 { deep: true }
选项来告诉 Vue 在监测数据变化时递归到嵌套属性中去。
当用户点击 Update User
按钮时,updateUser
函数会更新 user
属性中的 name
和 hobby
属性值。这里注意,我们使用了 data.user.name
和 data.user.hobby.push()
的方式来更新属性值,而不是直接赋值一个新的对象。这样就能够确保 user
属性的引用地址不变,从而让 deep
选项生效。
此时,由于我们监测了 user
属性,并使用了 { deep: true }
选项,当 user
属性中的嵌套属性值发生变化时,watch
回调函数就会被触发执行,打印出相应的信息。
❗注意:深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
即时回调的侦听器
watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以通过传入 immediate: true
选项来强制侦听器的回调立即执行:
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })
侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 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
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。在 setup()
或 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。
一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子:
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手动停止一个侦听器,请调用 watch
或 watchEffect
返回的函数:
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
通过上面的内容和例子,我们算是简单了解和学习了计算属性和侦听器,在前端开发的面试中,计算属性和侦听器也是经常出现的考核点,计算属性和侦听器的区别
则是面试官经常问道的题目。通过上面的内容,我们也大概可以把这些区别说明白了,接下来我们简单总结一下。
❓问:Vue 中计算属性和侦听器的区别是什么?
答:如下。
计算属性是基于它所依赖的响应式数据动态计算的值,只有在计算属性所依赖的数据发生变化时才会重新计算,且计算结果会被缓存起来。相比之下,侦听器监听的是某个特定的数据变化,当该数据变化时,会触发执行回调函数。
计算属性定义的是 getter
函数,只能用于获取数据,不能用于设置数据。而侦听器定义的是回调函数,可以进行任何逻辑处理,包括修改数据本身或者执行其他操作。
在使用场景上,计算属性通常用于将数据转换为显示需要的格式,而侦听器则适合处理需要执行异步或复杂逻辑的情况。
计算属性 computed
和侦听器 watch
都是实现响应式编程的重要工具,可以方便地处理复杂的业务数据变化场景。了解它们的区别和应用场景,能够帮助我们更好地设计和优化我们的 Vue 组件和应用。