Vue 3中监控响应式变量的几种方式

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函数还有以下优点:

  1. 当需要监听多个数据源的变化时,无需手动维护所依赖的数据源,降低开发时的心智负担。
  2. 当需要监听一个响应式对象的多个属性时,使用watchEffect会比使用deep watch更高效,因为它只会跟踪使用到的属性,不会递归跟踪所有的属性。

注意事项

使用watchEffect创建监听器时请在同步环境下使用,避免在异步情况下使用watchEffect。

setTimeout(() => {
  watchEffect(() => {})
}, 100);

上面的代码有两处缺陷:

  1. watchEffect只会在同步时才会收集依赖,异步使用时不能收集到全部的依赖。
  2. 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✔️✔️,自动进行✔️自动收集依赖并执行副作用、自动进行初始化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OriginCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值