监听和计算属性
计算属性
我们在第一节课讲了在模板语法中可以写js表达式,例如下面这样:
<div>三元运算:{{ bool ? '真' : '假' }}</div>举个常见的例子,我们想在页面上显示一个用户全名,但我们的数据库存储的数据通常是这样
{
"firstName": "张",
"lastName": "三",
"sex":"男"
}此时我们若是想要在页面上展示全名,可以这么写
<div>全名:{{ obj.firstName+obj.lastName }}</div>模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说我们变更下需求:根据用户的性别在用户的全名后面加上先生/女士
<div>全名:{{ obj.firstName + obj.lastName + (obj.sex == '男' ? '先生' : '女士') }}</div>完整的代码如下:
<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>现在再看模板的表达式插值就有些复杂了。我们必须认真看好一会儿才能明白它。更重要的是,如果在页面的很多位置都要显示用户全名,同样的代码我们要写很多遍,所以我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:
<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
例如下面这样
console.log(fullName.value);需要注意的是:默认情况下computed的value是只读的,若是直接对其赋值,代码和控制台会抛出警告,并且值不会被更改,页面也不会响应式更新
fullName.value="李四女士"; //[Vue warn] Write operation failed: computed value is readonly计算属性和函数的区别
看到这里,有一部分同学可能会有疑问,既然computed也是通过一个函数来返回值,那么它和普通的js函数有什么区别呢?
例如下面的js函数不也能实现一样的效果吗?
<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函数可以让我们的项目有更好的性能
计算属性只会更新响应式数据的计算
假设要获取当前的时间信息,因为不是响应式数据,所以这种情况下就需要用普通的函数去获取返回值,才能拿到最新的时间。
<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>可写计算属性
我们刚才说计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。
但是某些特殊场景中我们需要用到“可写”的属性,这时候就需要通过同时提供 getter 和 setter 来创建计算属性:
<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 就是 computed 的 getter ,跟原来传入 callback 的形式一样,用于 fullName.value 的读取,所以这里必须有明确的返回值。
这里的 set 就是 computed 的 setter ,它会接收一个参数,代表新的值,当通过 fullName.value = xxx赋值的时候,赋入的这个值,就会通过这个入参传递进来,可以根据的业务需要,把这个值赋给相关的数据源。
从计算属性返回的值是派生状态。可以把它看作是一个临时快照,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。
监听
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些副作用:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
watch
在vue中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数
watch的使用场景包括但不限于以下几种情况:
监听数据变化并执行相应操作:当需要在特定数据发生变化时执行一些逻辑操作时,可以使用watch。例如,监听表单输入字段的变化,当用户输入改变时触发某个操作,比如实时搜索、自动保存等。
异步操作和依赖关系处理:有时候需要在某个数据变化后执行异步操作,或者在多个数据之间建立依赖关系。watch可以用来监听数据的变化,并在数据变化后执行异步操作,例如发送网络请求、更新数据等。
监听计算属性的变化:Vue中的计算属性是根据响应式数据进行计算得出的属性。当计算属性依赖的响应式数据发生变化时,计算属性的值也会相应变化。使用watch可以监听计算属性的变化,并在计算属性发生改变时执行相应操作。
监听嵌套对象或数组的变化:对于嵌套对象或数组,可以使用deep选项来深度监听其内部值的变化。这样可以在对象或数组的值发生改变时触发回调函数,进行相应的处理。
基本写法
import { watch } from 'vue'
watch(
source, // 必传,要侦听的数据源
callback, // 必传,侦听到变化后要执行的回调函数
options // 可选,一些侦听选项
)举个常见的例子:查询一个分页表格的数据,当页码变化了之后调用接口获取最新数据
<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 是一个对象的形式传入,有以下几个选项:
| 选项 | 类型 | 默认值 | 可选值 | 作用 |
|---|---|---|---|---|
| deep | boolean | false | true | false | 是否进行深度侦听 |
| immediate | boolean | false | true | false | 是否立即执行侦听回调 |
| flush | string | 'pre' | 'pre' | 'post' | 'sync' | 控制侦听回调的调用时机 |
| onTrack | (e) => void | 在数据源被追踪时调用 | ||
| onTrigger | (e) => void | 在侦听回调被触发时调用 |
deep
deep 选项接受一个布尔值,可以设置为 true 开启深度侦听,或者是 false 关闭深度侦听,或者是 false 关闭深度侦听,默认情况下这个选项是 false 关闭深度侦听的,但也存在特例。
设置为 false 的情况下,如果直接侦听一个响应式的 引用类型 数据(e.g. Object 、 Array … ),虽然它的属性的值有变化,但对其本身来说是不变的,因为内存地址没变,所以不会触发 watch 的 callback 。
下面是一个未开启深度侦听的例子:
<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属性,使其可以深度侦听
<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:
<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 选项来强制侦听器的回调立即执行:
watch(
source,
(newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
},
{ immediate: true }
)侦听数据源类型
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组(批量侦听):
getter函数
注意,你不能直接侦听响应式对象的属性值,例如:
const obj_person = reactive({ name: '李雷' });
watch(obj_person.name, (newValue: any, oldValue: any) => {
console.log(`监听到了变化,变化前是${oldValue},变化后是${newValue}`);
});这里需要用一个返回该属性的 getter 函数:
// 提供一个 getter 函数
const obj_person = reactive({ name: '李雷' });
watch(() => obj_person.name, (newValue: any, oldValue: any) => {
console.log(`监听到了变化,变化前是${oldValue},变化后是${newValue}`);
});批量侦听
如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,第一反应可能是这样来写:
- 抽离相同的处理行为为公共函数
- 然后定义多个侦听操作,传入这个公共函数
<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本身提供了一个批量侦听的用法,和基础用法的区别在于,数据源和回调参数都变成了数组的形式。
数据源:以数组的形式传入,里面每一项都是一个响应式数据。
回调参数:原来的 oldValue 和 newValue 也都变成了数组,每个数组里面的顺序和数据源数组排序一致。
我们改造一下上面的例子:
<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批量侦听的例子
<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>它会立即执行传入的一个函数(和watch的immediate: true一个效果),同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
和 watch 的区别
虽然理论上 watchEffect 是 watch 的一个简化操作,可以用来代替批量侦听 ,但它们也有一定的区别:
watch可以访问侦听状态变化前后的值,而watchEffect没有。watch是在属性改变的时候才执行,而watchEffect则默认会执行一次,然后在属性改变的时候也会执行。watch只追踪明确侦听的数据源,而watchEffect则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。