计算属性 computed()
推荐使用计算属性来描述依赖响应式状态的复杂逻辑
- 计算属性默认是只读的,
computed()
方法期望接收一个 getter 函数,返回值为一个计算属性 ref,模版中会自动解包;- 计算属性值会基于其响应式依赖被缓存;
- 计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。不要在 getter 中做异步请求或者更改 DOM!
- 避免直接修改计算属性值",从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的;
vue
<script setup>
import { ref, computed } from "vue";
const firstName = ref("John");
const lastName = ref("Doe");
const fullName = computed({
// getter
get() {
return firstName.value + " " + lastName.value;
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(" ");
},
});
const author = reactive({
name: "John Doe",
books: [
"Vue 2 - Advanced Guide",
"Vue 3 - Basic Guide",
"Vue 4 - The Mystery",
],
});
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? "Yes" : "No";
});
</script>
现在当你再运行 fullName.value = 'John Doe'
时,setter 会被调用而 firstName 和 lastName 会随之更新。
侦听器 watch()
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
侦听数据源类型: watch
的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)
、一个响应式对象
、一个 getter 函数
、或多个数据源组成的数组
js
const x = ref(0);
const y = ref(0);
const obj = reactive({ count: 0 });
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`);
});
// 一个响应式对象
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
});
// 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}`);
});
注意,你不能直接侦听响应式对象的属性值,需要用一个返回该属性的 getter 函数:
js
const obj = reactive({ count: 0 });
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`);
});
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`);
}
);
// 直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——
// 该回调函数在所有嵌套的变更时都会被触发
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
});
// 相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
);
深层侦听器和立即执行
深层侦听器 :直接给 watch()
传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发,相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
js
// 响应式对象
const obj = reactive({ count: 0 });
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
});
obj.count++;
// 响应式对象的 getter 函数
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
);
// 深层侦听器
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
);
即时回调的侦听器:watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以通过传入 immediate: true
选项来强制侦听器的回调立即执行:
js
watch(
source,
(newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
},
{ immediate: true }
);
侦听器 watchEffect()
watch()
是懒执行的:仅当数据源变化时,才会执行回调watchEffect()
会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,自动分析出响应源。watch
只追踪明确侦听的数据源。watchEffect
,则会在副作用发生期间追踪依赖。
回调的触发时机
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前
被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post'
选项:
js
import { watchPostEffect } from "vue";
watch(source, callback, {
flush: "post",
});
watchEffect(callback, {
flush: "post",
});
// 后置刷新 watchPostEffect()
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
});
停止侦听器
在 setup()
或 <script setup>
中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。
一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。
vue
<script setup>
import { watchEffect } from "vue";
// 它会自动停止 同步创建的
watchEffect(() => {});
// ...这个则不会! 异步创建的
setTimeout(() => {
watchEffect(() => {});
}, 100);
const unwatch = watchEffect(() => {});
// ...当该侦听器不再需要时
unwatch();
// 需要异步请求得到的数据
const data = ref(null);
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
});
</script>