计算属性是基于响应式数据进行计算得出的结果并被缓存的属性。在组件的模板中可以像数据属性一样使用,它由一个计算函数和它所依赖的数据组成,只有当所依赖的数据发生变化时,它才会重新计算属性的值。Vue.js 内部实现了缓存机制,因此计算属性只会在必要的时候才重新计算。这样能够提高 Vue.js 应用的性能,并且让代码更加简洁和易于维护。
在 Vue 组件中定义计算属性,需要在 computed
属性中声明一个或多个计算函数。计算函数中使用 return
语句返回计算结果,Vue中的计算属性有两种写法,一种是只读计算属性,一种是可读写计算属性。
顾名思义,只读计算属性只能读取计算属性的值,不能对计算属性进行写操作。计算属性默认是只读的。如下面的示例:
<template>
<p>单价:{{ count }}</p>
<span>总价:{{ double }}</span>
</template>
<script setup>
import { reactive,ref, computed } from 'vue'
const count = ref(5)
// 一个计算属性 ref
const double = computed(() => {
return count.value * 2
})
</script>
<style scoped>
</style>
可读计算属性,需要我们通过同时提供 getter 和 setter 来创建,书写方式如下代码所示:
<template>
<p>姓:{{ firstName }}</p>
<span>名:{{ lastName }}</span>
<p>姓名:{{ fallName }}</p>
</template>
<script setup>
import { reactive,ref, computed } from 'vue'
const firstName = ref('李')
const lastName = ref('大锤')
// 一个计算属性 ref
const fallName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>
<style scoped>
</style>
Vue.js 内部会对计算属性进行缓存,保证计算属性只在必要的时候才会重新计算。在多个依赖同一个计算属性的组件中,计算属性只会在它们之间共享一个实例。这样可以提高应用的性能,并且减少重复计算的开销。
侦听器是用来响应数据的变化,并在变化时执行一些操作。相比计算属性,侦听器更加灵活,可以处理更为复杂的逻辑。例如在数据变化时发送 Ajax 请求、执行复杂的计算或者更新一些持久化数据。
在 Vue 组件中定义侦听器,需要在 watch
属性中声明一个或多个侦听函数。每个侦听函数接收两个参数,第一个参数是新的数据值,第二个参数是旧的数据值。
写一个监听price变化的侦听器,代码如下
<template>
<div>
<p>股票价格:{{ price }}</p>
<button @click="price += 10">增加价格</button>
</div>
</template>
<script setup>
import { ref,watch } from 'vue'
const price = ref(100)
watch(price,(newValue, oldValue)=>{
console.log(`股票价格从 ${oldValue} 变为 ${newValue}`);
})
</script>
<style scoped>
</style>
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
注意
不能直接侦听响应式对象的属性值,例如如下代码是错误的:
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
这里正确的写法是需要用一个返回该属性的 getter 函数:
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
默认情况下,Vue提供的侦听器是浅层侦听,只有在被侦听的对象或数组本身发生变化时才会执行侦听函数。如果需要深度侦听一个对象或数组中嵌套的数据变化,就需要深度侦听。
在Vue3中,有两种方式可以开启深度侦听
在Vue3中,直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
<template>
<div>
<p>{{state.count.a.b}}</p>
<button @click="state.count.a.b++">add</button>
</div>
</template>
<script setup>
import { ref,reactive, watch } from 'vue'
const state = reactive({
count: {
a: {
b: 1
}
}
})
watch(state.count, (count, prevCount) => {
console.log(count)
})
</script>
<style scoped></style>
这里,我们利用 reactive API 创建了一个嵌套层级较深的响应式对象 state,然后再调用 watch API 侦听 state.count 的变化。接下来我们修改内部属性 state.count.a.b 的值,你会发现 watcher 的回调函数执行了,为什么会执行呢?
原则上Proxy实现的响应式对象,只有对象属性先被访问触发了依赖收集,再去修改这个属性,才可以通知对应的依赖更新。而从上述业务代码来看,我们修改 state.count.a.b 的值时并没有访问它 ,但还是触发了 watcher 的回调函数。
根本原因是,当我们执行 watch 函数的时候,我们知道如果侦听的是一个 reactive 对象,那么内部会设置 deep 为 true, 然后执行 traverse 去递归访问对象深层子属性,这个时候就会访问 state.count.a.b 触发依赖收集,这里收集的依赖是 watcher 内部创建的 effect runner。因此,当我们再去修改 state.count.a.b 的时候,就会通知这个 effect ,所以最终会执行 watcher 的回调函数。
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
我们也可以使用deep选项来强制转成深层侦听,代码格式如下:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。比如,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。这是,我们需要设置侦听器的另一个参数:immediate,我们通过设置immediate: true 选项来强制侦听器的回调立即执行。代码格式如下:
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })
watchEffect() 是一个响应式数据的监听函数,可以监听响应式数据的变化并自动执行一段关联代码,根据监听的数据变化而自动计算和更新数据。
与 watch() 函数不同,watchEffect() 不需要显式地声明依赖关系,而是会在执行关联代码时自动建立依赖关系,并在依赖数据变化时重新执行关联代码。
watchEffect() 函数的使用方法如下:
<template>
<div>
<p>{{ count }}</p>
<button @click="count++">add</button>
</div>
</template>
<script setup>
import { ref, reactive, watch, watchEffect } from 'vue'
const count = ref(0);
watchEffect(() => {
console.log(`count is ${count.value}`);
});
</script>
<style scoped></style>
在上面的示例中,定义了一个 count 变量和一个 watchEffect() 监听函数,watchEffect() 函数的参数是一个侦听函数(箭头函数),当监听的响应式数据(count)发生变化时,watchEffect() 会立即执行侦听函数,并自动建立依赖关系。
watchEffect() 函数还支持返回一个清理函数,用于在组件销毁时取消监听。如下所示:
<template>
<div>
<p>{{ count }}</p>
<button @click="count++">add</button>
</div>
</template>
<script setup>
import { ref, reactive, watch, watchEffect,onUnmounted } from 'vue'
const count = ref(0);
const stop = watchEffect(() => {
console.log(`count is ${count.value}`);
});
onUnmounted(stop); // 组件销毁时取消监听
</script>
<style scoped></style>
在上面的示例中,使用 stop 变量存储 watchEffect() 函数返回的清理函数,并在组件销毁时调用 onUnmounted() 函数来取消监听。
watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
计算属性和侦听器都是用来做响应式数据处理的方法,都可以监听某个变量的变化并做出相应的处理。