Skip to content

监听和计算属性

计算属性

我们在第一节课讲了在模板语法中可以写js表达式,例如下面这样:

vue
<div>三元运算:{{ bool ? '真' : '假' }}</div>

举个常见的例子,我们想在页面上显示一个用户全名,但我们的数据库存储的数据通常是这样

json
{
	"firstName": "张",
	"lastName": "三",
    "sex":"男"
}

此时我们若是想要在页面上展示全名,可以这么写

vue
<div>全名:{{ obj.firstName+obj.lastName }}</div>

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说我们变更下需求:根据用户的性别在用户的全名后面加上先生/女士

vue
<div>全名:{{ obj.firstName + obj.lastName + (obj.sex == '男' ? '先生' : '女士') }}</div>

完整的代码如下:

vue
<template>
  <div>
    <div>全名:{{ obj.firstName + obj.lastName + (obj.sex == '男' ? '先生' : '女士') }}</div>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';

const obj = ref({
  "firstName": "张",
  "lastName": "三",
  "sex": "男"
});
</script>

现在再看模板的表达式插值就有些复杂了。我们必须认真看好一会儿才能明白它。更重要的是,如果在页面的很多位置都要显示用户全名,同样的代码我们要写很多遍,所以我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:

vue
<template>
  <div>
    <div>全名:{{ fullName }}</div>
  </div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';

const obj = ref({
  "firstName": "张",
  "lastName": "三",
  "sex": "男"
});

const fullName = computed(() => obj.value.firstName + obj.value.lastName + (obj.value.sex == '男' ? '先生' : '女士'))
</script>

computed就是我们本次要学习的计算属性,computed()方法期望接收一个getter函数,返回值为一个计算属性ref。它可以通过现有的响应式数据,去通过计算得到新的响应式变量

我们上面代码的fullName就是定义出来的计算属性,它和ref一样,在script标签内想要取值的话,需要加上.value

例如下面这样

js
console.log(fullName.value);

需要注意的是:默认情况下computedvalue是只读的,若是直接对其赋值,代码和控制台会抛出警告,并且值不会被更改,页面也不会响应式更新

js
fullName.value="李四女士"; //[Vue warn] Write operation failed: computed value is readonly

计算属性和函数的区别

看到这里,有一部分同学可能会有疑问,既然computed也是通过一个函数来返回值,那么它和普通的js函数有什么区别呢?

例如下面的js函数不也能实现一样的效果吗?

vue
<template>
  <div>
    <div>全名:{{ fullName() }}</div>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';

const obj = ref({
  "firstName": "张",
  "lastName": "三",
  "sex": "男"
});

const fullName = () => obj.value.firstName + obj.value.lastName + (obj.value.sex == '男' ? '先生' : '女士')


console.log(fullName());
</script>

两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。

还是以上面的代码说明,如果是计算属性的写法,只要它的响应式依赖(firstName,lastName,sex)没有更新,不管你访问多少次fullName,都会直接返回上一次计算好的结果,但是如果是函数写法,只要你调用一次fullName(),对应的表达式就会重新运行一次,所以计算属性对比普通的js函数可以让我们的项目有更好的性能

计算属性只会更新响应式数据的计算

假设要获取当前的时间信息,因为不是响应式数据,所以这种情况下就需要用普通的函数去获取返回值,才能拿到最新的时间。

vue
<template>
  <div>
    <button @click="getNowTime">拿最新时间</button>
  </div>
</template>
<script setup lang="ts">
import { computed } from 'vue';

const nowTime1 = computed(() => new Date());//调用它无法拿到最新的时间

const nowTime2 = () => new Date(); //调用它可以拿到最新时间

const getNowTime = () => {
  console.log(nowTime1.value, nowTime2(), '最新时间对比');

}
</script>

可写计算属性​

我们刚才说计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。

但是某些特殊场景中我们需要用到“可写”的属性,这时候就需要通过同时提供 gettersetter 来创建计算属性:

vue
<template>
  <div>
    <div>全名:{{ fullName }}</div>
    <button @click="testChangeFullName">更改全名</button>
  </div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';

const obj = ref({
  "firstName": "张",
  "lastName": "三",
  "sex": "男"
});

const fullName = computed({
  get() {
    return obj.value.firstName + obj.value.lastName + (obj.value.sex == '男' ? '先生' : '女士');
  },
  set(val) {
    const val_arr = val.split(' ');
    obj.value.firstName = val_arr[0];
    obj.value.lastName = val_arr[1];
    obj.value.sex = val_arr[2];
  }
}
);

const testChangeFullName = () => {
  fullName.value = '李 四 女士';
}

</script>

这里的 get 就是 computedgetter ,跟原来传入 callback 的形式一样,用于 fullName.value 的读取,所以这里必须有明确的返回值。

这里的 set 就是 computedsetter ,它会接收一个参数,代表新的值,当通过 fullName.value = xxx赋值的时候,赋入的这个值,就会通过这个入参传递进来,可以根据的业务需要,把这个值赋给相关的数据源。

从计算属性返回的值是派生状态。可以把它看作是一个临时快照,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。

监听

计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些副作用:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

watch

vue中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数

watch的使用场景包括但不限于以下几种情况:

  • 监听数据变化并执行相应操作:当需要在特定数据发生变化时执行一些逻辑操作时,可以使用watch。例如,监听表单输入字段的变化,当用户输入改变时触发某个操作,比如实时搜索、自动保存等。

  • 异步操作和依赖关系处理:有时候需要在某个数据变化后执行异步操作,或者在多个数据之间建立依赖关系。watch可以用来监听数据的变化,并在数据变化后执行异步操作,例如发送网络请求、更新数据等。

  • 监听计算属性的变化:Vue中的计算属性是根据响应式数据进行计算得出的属性。当计算属性依赖的响应式数据发生变化时,计算属性的值也会相应变化。使用watch可以监听计算属性的变化,并在计算属性发生改变时执行相应操作。

  • 监听嵌套对象或数组的变化:对于嵌套对象或数组,可以使用deep选项来深度监听其内部值的变化。这样可以在对象或数组的值发生改变时触发回调函数,进行相应的处理。

基本写法

js
import { watch } from 'vue'

watch(
  source, // 必传,要侦听的数据源
  callback, // 必传,侦听到变化后要执行的回调函数
  options // 可选,一些侦听选项
)

举个常见的例子:查询一个分页表格的数据,当页码变化了之后调用接口获取最新数据

vue
<template>
  <div>
    <select v-model="current_page">
      <option :value="1">第一页</option>
      <option :value="2">第二页</option>
      <option :value="3">第三页</option>
      <option :value="4">第四页</option>
      <option :value="5">第五页</option>
    </select>
    <div>
      <button @click="current_page = current_page - 1">跳转到上一页</button>
      <button @click="current_page = current_page + 1">跳转到下一页</button>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';

const current_page = ref(1);

watch(current_page, (newValue: number, oldValue: number) => {
  console.log(`当前页码发生了变化,变化前是${oldValue},变化后是${newValue}`);
  // 拿到最新页码之后做一些事
});
</script>

options 是一个对象的形式传入,有以下几个选项:

选项类型默认值可选值作用
deepbooleanfalsetrue | false是否进行深度侦听
immediatebooleanfalsetrue | false是否立即执行侦听回调
flushstring'pre''pre' | 'post' | 'sync'控制侦听回调的调用时机
onTrack(e) => void在数据源被追踪时调用
onTrigger(e) => void在侦听回调被触发时调用
deep

deep 选项接受一个布尔值,可以设置为 true 开启深度侦听,或者是 false 关闭深度侦听,或者是 false 关闭深度侦听,默认情况下这个选项是 false 关闭深度侦听的,但也存在特例。

设置为 false 的情况下,如果直接侦听一个响应式的 引用类型 数据(e.g. ObjectArray … ),虽然它的属性的值有变化,但对其本身来说是不变的,因为内存地址没变,所以不会触发 watchcallback

下面是一个未开启深度侦听的例子:

vue
<template>
  <div>
    <div>当前对象的name是:{{ obj_person.name }}</div>

    <button @click="obj_person.name = '韩梅梅'">把名字改成韩梅梅</button>

  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';

const obj_person = ref({ name: '李雷' });

watch(obj_person, (newValue: any, oldValue: any) => {
  console.log(`监听到了变化,变化前是${oldValue.name},变化后是${newValue.name}`);
});
</script>

此时点击按钮,页面上的李雷会被替换成韩梅梅,但是控制台没有打印出信息,证明没有执行watch的回调函数

我们在下面加上deep属性,使其可以深度侦听

vue
<template>
  <div>
    <div>当前对象的name是:{{ obj_person.name }}</div>

    <button @click="obj_person.name = '韩梅梅'">把名字改成韩梅梅</button>

  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';

const obj_person = ref({ name: '李雷' });

watch(obj_person, (newValue: any, oldValue: any) => {
  console.log(`监听到了变化,变化前是${oldValue.name},变化后是${newValue.name}`);
}, 
{
  deep: true
});
</script>

注意!!!

当监听的数据源是一个对象时,当其嵌套属性发生更改时,newValue 此处和 oldValue 是相等的。因为它们是同一个对象

reactive无需开启设置deep:true

这个情况就是上面所说的特例,因为reactive会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发,即便是你手动设置为false:

vue
<template>
  <div>
    <div>当前对象的name是:{{ obj_person.name }}</div>

    <button @click="obj_person.name = '韩梅梅'">把名字改成韩梅梅</button>

  </div>
</template>
<script setup lang="ts">
import { reactive, watch } from 'vue';

const obj_person = reactive({ name: '李雷' });

watch(obj_person, (newValue: any, oldValue: any) => {
  console.log(`监听到了变化,变化前是${oldValue.name},变化后是${newValue.name}`);
},
  {
    deep: false
  });
</script>
immediate

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

这个时候就可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

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

侦听数据源类型

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组(批量侦听):

getter函数

注意,你不能直接侦听响应式对象的属性值,例如:

js
const obj_person = reactive({ name: '李雷' });

watch(obj_person.name, (newValue: any, oldValue: any) => {
  console.log(`监听到了变化,变化前是${oldValue},变化后是${newValue}`);
});

这里需要用一个返回该属性的 getter 函数:

js
// 提供一个 getter 函数
const obj_person = reactive({ name: '李雷' });

watch(() => obj_person.name, (newValue: any, oldValue: any) => {
  console.log(`监听到了变化,变化前是${oldValue},变化后是${newValue}`);
});

批量侦听

如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,第一反应可能是这样来写:

  1. 抽离相同的处理行为为公共函数
  2. 然后定义多个侦听操作,传入这个公共函数
vue
<template>
  <div>
    <button @click="str = 'Hello World'">更改str</button>
    <button @click="num++">更改num</button>
  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';

const str = ref('World Hello')
const num = ref(0)

// 抽离相同的处理行为为公共函数
const handleWatch = (
  newValue: any,
  oldValue: any
) => {
  console.log(`监听到了变化,变化前是${oldValue},变化后是${newValue}`);
}

// 然后定义多个侦听操作,传入这个公共函数
watch(str, handleWatch)
watch(num, handleWatch)
</script>

这样写其实没什么问题,不过除了抽离公共代码的写法之外, watch本身提供了一个批量侦听的用法,和基础用法的区别在于,数据源和回调参数都变成了数组的形式。

数据源:以数组的形式传入,里面每一项都是一个响应式数据。

回调参数:原来的 oldValuenewValue 也都变成了数组,每个数组里面的顺序和数据源数组排序一致。

我们改造一下上面的例子:

vue
<template>
  <div>
    <button @click="str = 'Hello World'">更改str</button>
    <button @click="num++">更改num</button>
  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const str = ref('World Hello')
const num = ref(0)
watch([str, num], ([newStr, newNum], [oldStr, oldNum]) => {
  console.log('str的变化', { newStr, oldStr })
  console.log('num的变化', { newNum, oldNum })
})
</script>

watchEffect

如果一个函数里包含了多个需要侦听的数据,一个一个数据去侦听太麻烦了,在 Vue 3 里,可以直接使用 watchEffect API来简化的操作。

基本写法

我们直接改造上面用watch批量侦听的例子

vue
<template>
  <div>
    <button @click="str = 'Hello World'">更改str</button>
    <button @click="num++">更改num</button>
  </div>
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue';

const str = ref('World Hello')
const num = ref(0)

watchEffect(() => {
  console.log('str的值:', str.value)
  console.log('num的值', num.value)
})
</script>

它会立即执行传入的一个函数(和watchimmediate: true一个效果),同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

和 watch 的区别

虽然理论上 watchEffectwatch 的一个简化操作,可以用来代替批量侦听 ,但它们也有一定的区别:

  1. watch 可以访问侦听状态变化前后的值,而 watchEffect 没有。
  2. watch 是在属性改变的时候才执行,而 watchEffect 则默认会执行一次,然后在属性改变的时候也会执行。
  3. watch 只追踪明确侦听的数据源,而watchEffect则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。