Vue 3中监控响应式变量的几种方式
注意:下面所有的代码都基于Composition API,并使用script setup方法和Typescript语言编写,其SFC中逻辑部分编写方法为<script setup lang=”ts”>
。
使用computed监控数据变化
使用场景
在Vue中,可以使用模板将响应式变量呈现在页面上,同时也可以使用一些简单的表达式对响应式变量进行处理。但是当表达式的逻辑变得复杂时,模板将变得难以维护,因此可以使用计算属性描述一些依赖于响应式变量的逻辑。
代码示例
import { ref, computed } from "vue";
const firstName = ref("John");
const lastName = ref("Smith");
const fullName = computed<string>(() => {
return `${firstName}·${lastName}`;
});
// 在模板中使用
<div>{{ fullName }}</div>
其中computed的类型标注<string>
可以省略,因为computed函数会自动从函数中推导出返回值的类型。
计算属性和方法之间的区别
上面的代码可以使用方法实现同样的功能,代码如下:
import { ref, computed } from "vue";
const firstName = ref("John");
const lastName = ref("Smith");
function fullName(): string {
return `${firstName}·${lastName}`;
}
// 在模板中使用
<div>{{ fullName() }}</div>
大多数情况下推荐使用计算属性,因为computed会将计算结果进行缓存,只会在其依赖的响应式变量更新时才会重新计算。
当所依赖的响应式变量消耗很大的资源(例如迭代一个特别大的列表),且同时依赖于其他的响应式变量时,每次更新都会重新进行计算,产生额外的性能开销。
但是computed无法跟踪非响应式依赖的更新,对于如下代码,由于Date.now()
不是一个响应式变量,因此now永远不会被更新:
const now = computed(() => Date.now());
使用计算属性的最佳实践
computed不应该有副作用
根据文档,计算属性应该只进行计算,不能带有其他的副作用,因为计算属性只应该被用来根据描述如何从源数据生成派生数据。因此一些副作用比如异步请求和修改DOM等不应该放在computed中,关于如何根据响应式变量创建副作用,我们会在下一节讨论。
避免直接修改计算属性的值
计算属性相当于一个从源数据到计算结果的单项映射,因此修改计算属性在大多数情况下是没有必要的。
一个少见的例外是在Composition API中使用Vue I18n修改locale,参考如下代码,locale
就是一个可以修改的计算属性值:
import { useI18n } from "vue-i18n";
const { locale } = useI18n();
// locale.value = "xxx";
使用watch函数监控数据变化
使用场景
在前面我们提到要避免在computed函数中执行副作用,例如异步请求、修改DOM等。这时我们可以使用watch函数在其监听的响应式变量发生变化时执行副作用。
代码示例
同样以上面Vue I18n的使用为例,这次我们跟踪locale的变化:
import { useI18n } from "vue-i18n";
const messages = {
en: {
hello: "Hello, world!"
},
zhHans: {
hello: "你好,世界!"
}
}
const { t, locale } = useI18n({ messages });
// 回调函数既可以同时获取新旧值,也可以只获取新值或者什么都不获取
watch(locale, (newLocale, oldLocale) => {
console.log(oldLocale);
console.log(newLocale);
console.log(t("hello"));
});
// 假如当locale从en切换为zhHans,其输出如下:
// en
// zhHans
// 你好,世界!
watch函数监听函数的类型
watch函数可以监听:一个ref(包括计算属性)、一个响应式对象、一个getter函数或者多个数据源的数组,其代码示例如下:
const firstName = ref("John");
const lastName = ref("Smith");
// 监听一个ref
watch(firstName, () => {/* 回调函数省略,下同 */});
// 监听getter函数
watch(() => firstName.value + lastName.value, () => {});
// 监听多个数据源
watch([firstName, () => lastName.value], () => {});
使用watch函数监听响应式对象
const person = reactive({
name: "John Smith",
age: 20
});
watch(person, () => {});
上述代码会隐式地创建一个深层监听器(deep watcher),即当person的任何一个属性被更新时都会触发回调。但是像watch(person.name, …)
一样的代码不会按照预期工作,如果要监听响应式对象某个属性的变化,请使用getter函数:
watch(() => person.name, () => {});
也可以在创建watcher时手动指定deep: true
,强制创建一个深层监听器:
watch(() => person.name, () => {}, { deep: true });
**注意:**使用watch函数监听响应式对象时会嵌套遍历对象中的所有属性,当响应式对象结构很复杂时开销会增大,因此请谨慎使用并且留意应用性能。
立刻执行的watch函数
一般来说,watch函数只会在数据源发生变化时才会执行回调函数,但是有时候需要进行一些初始化操作,可以指定immediate: true
在创建watcher执行一次回调函数:
const bookId = ref(1);
const book = reactive({ name: "" });
watch(bookId, async () => {
const response = await fetch(`https://api.com/books/${bookId.value}`);
book.name = response.json().name;
}, { immediate: true });
使用watchEffect函数监控数据变化
前面讲述了如何使用watch函数在响应式数据发生变化时执行副作用,但是很多时候被监听的响应式变量同样会在回调函数中使用。就像前面的代码中,我们监听了bookId,然后又使用它获取了对应book的信息。我们可以使用watchEffect函数来简化这一写法,让Vue的运行时自动收集依赖:
watchEffect(() => {
const response = await fetch(`https://api.com/books/${bookId.value}`);
book.name = response.json().name;
});
在上面的代码中,在watcher创建时会自动执行一次回调函数,无需指定immediate: true
,它会自动收集回调函数中用到的依赖,并在这些依赖变化时自动执行回调。除此之外,watchEffect函数相对于watch函数还有以下优点:
- 当需要监听多个数据源的变化时,无需手动维护所依赖的数据源,降低开发时的心智负担。
- 当需要监听一个响应式对象的多个属性时,使用watchEffect会比使用deep watch更高效,因为它只会跟踪使用到的属性,不会递归跟踪所有的属性。
注意事项
使用watchEffect创建监听器时请在同步环境下使用,避免在异步情况下使用watchEffect。
setTimeout(() => {
watchEffect(() => {})
}, 100);
上面的代码有两处缺陷:
- watchEffect只会在同步时才会收集依赖,异步使用时不能收集到全部的依赖。
- 在
setup()
或<script setup>
中同步使用watchEffect时,创建的watcher会自动绑定到组件实例上,并在组件销毁时自动停止,而异步创建的watcher不会绑定到组件实例上,可能造成内存泄漏,不过可以使用以下代码手动停止监听:
const unwatch = watchEffect(() => {});
unwatch();
在大多数情况下,异步使用watchEffect都是不必要的,如果需要在创建时等待一些数据的初始化,可以在同步环境下进行判断:
const data = ref(null);
watchEffect(() => {
if (data.value) {
// 在数据加载后进行监听,并执行回调
}
});
watch和watchEffect回调函数的执行时机
默认情况下,使用watch和watchEffect创建的监听器的回调函数,会在Vue组件跟新之前被调用,这意味着此时访问到的DOM都会是被更新之前的状态。如果想在DOM被更新之后调用回调函数,请指定flush: "post"
,或者使用watchEffectPost
函数:
watch(source, callback, { flush: "post" });
// 下面二者是等价写法
watchEffect(callback, { flush: "post" });
watchEffectPost(callback);
在某些情况下,如果需要在响应式依赖发生变化时立即执行回调函数(比如使缓存失效),可以指定flush: “sync”
,或者使用watchEffectSync
函数:
watch(source, callback, { flush: "sync" });
// 下面二者是等价写法
watchEffect(callback, { flush: "sync" });
watchEffectSync(callback);
**注意:**请谨慎使用sync watchers,如果有多个属性同时更新时,这会导致性能问题和数据一致性问题。
三种方法之间的不同点
监控响应式变量的方法 | 自动收集依赖 | 执行副作用 | 在创建时执行初始化操作 | 控制回调执行的时机 | 特点 |
---|---|---|---|---|---|
computed | ✔️ | ❌ | ❌ | 不支持回调 | 实现最简单,无法执行副作用 |
watch | ✔️ | ✔️ | ✔️ | ✔️ | 可以执行副作用,需要手动指定依赖 |
watchEffect | ❌ | ✔️ | ✔️,自动进行 | ✔️ | 自动收集依赖并执行副作用、自动进行初始化 |