vue中的计算属性和侦听器

Vue.js 中,计算属性和侦听器是两种常用的动态数据处理方法,它们可以帮助我们更方便地响应数据的变化。今天我们就来聊一聊这两种方法的写法和用法,并比较它们之间的异同。

计算属性

计算属性是基于响应式数据进行计算得出的结果并被缓存的属性。在组件的模板中可以像数据属性一样使用,它由一个计算函数和它所依赖的数据组成,只有当所依赖的数据发生变化时,它才会重新计算属性的值。Vue.js 内部实现了缓存机制,因此计算属性只会在必要的时候才重新计算。这样能够提高 Vue.js 应用的性能,并且让代码更加简洁和易于维护。

使用计算属性

在 Vue 组件中定义计算属性,需要在 computed 属性中声明一个或多个计算函数。计算函数中使用 return 语句返回计算结果,Vue中的计算属性有两种写法,一种是只读计算属性,一种是可读写计算属性。

只读计算属性

顾名思义,只读计算属性只能读取计算属性的值,不能对计算属性进行写操作。计算属性默认是只读的。如下面的示例:

代码语言:javascript
复制
<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 来创建,书写方式如下代码所示:

代码语言:javascript
复制
<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变化的侦听器,代码如下

代码语言:javascript
复制
<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 函数、或多个数据源组成的数组:

代码语言:javascript
复制
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})
})

注意
不能直接侦听响应式对象的属性值,例如如下代码是错误的:

代码语言:javascript
复制
const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(count is: ${count})
})

这里正确的写法是需要用一个返回该属性的 getter 函数:

代码语言:javascript
复制
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(count is: ${count})
}
)

深度侦听

默认情况下,Vue提供的侦听器是浅层侦听,只有在被侦听的对象或数组本身发生变化时才会执行侦听函数。如果需要深度侦听一个对象或数组中嵌套的数据变化,就需要深度侦听。
在Vue3中,有两种方式可以开启深度侦听

直接给 watch() 传入一个响应式对象

在Vue3中,直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

代码语言:javascript
复制
<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 选项,强制转成深层侦听器

我们也可以使用deep选项来强制转成深层侦听,代码格式如下:

代码语言:javascript
复制
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:newValue 此处和 oldValue 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)

立即侦听

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。比如,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。这是,我们需要设置侦听器的另一个参数:immediate,我们通过设置immediate: true 选项来强制侦听器的回调立即执行。代码格式如下:

代码语言:javascript
复制
watch(source, (newValue, oldValue) => {
// 立即执行,且当 source 改变时再次执行
}, { immediate: true })

watchEffect()

watchEffect() 是一个响应式数据的监听函数,可以监听响应式数据的变化并自动执行一段关联代码,根据监听的数据变化而自动计算和更新数据。

与 watch() 函数不同,watchEffect() 不需要显式地声明依赖关系,而是会在执行关联代码时自动建立依赖关系,并在依赖数据变化时重新执行关联代码。

watchEffect() 函数的使用方法如下:

代码语言:javascript
复制
<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() 函数还支持返回一个清理函数,用于在组件销毁时取消监听。如下所示:

代码语言:javascript
复制
<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的区别

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

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
  • 侦听的源不同 。 watch 可以侦听一个或多个响应式对象,也可以侦听一个 getter 函数,而 watchEffect 侦听的是一个普通函数,只要内部访问了响应式对象即可,这个函数并不需要返回响应式对象。
  • 没有回调函数 。 watchEffect没有回调函数,副作用函数的内部响应式对象发生变化后,会再次执行这个副作用函数。
  • 立即执行 。 watchEffect在创建好 watcher 后,会立刻执行它的副作用函数,而 watch 需要配置 immediate 为 true,才会立即执行回调函数。

计算属性和侦听器的异同点

相同点

计算属性和侦听器都是用来做响应式数据处理的方法,都可以监听某个变量的变化并做出相应的处理。

不同点

  • 计算属性是根据其他数据计算出新数据的方法,侦听器是监听某个变量的变化并做出相应的处理的方法。
  • 计算属性的返回值会被缓存,只有依赖数据变化时才会重新计算,而侦听器在每次变化时都会被调用。
  • 计算属性适用于在模板中只需要调用结果的情况,尤其是计算逻辑相对简单,直接依赖单一响应式数据的情况。侦听器适用于需要根据多个响应式数据计算得出结果或需要进行更加复杂的逻辑处理的情况。
  • 计算属性支持 Getter 和 Setter 方法,可以实现数据的双向绑定。而侦听器只能进行数据的单向绑定。