Vue Composition API watch与computed的监听与计算属性实现
Vue Composition API 之 watch 与 computed 深入解析
在 Vue 的开发中,watch
和 computed
是两个非常重要的特性,它们在数据处理和响应式系统中扮演着关键角色。在 Vue Composition API 中,这两个特性依然存在,并且以一种新的方式进行使用,为开发者提供了更灵活和强大的数据处理能力。
watch 的使用与原理
watch
主要用于监听数据的变化,并在数据变化时执行相应的操作。在 Vue Composition API 中,watch
的使用方式与传统的 Vue 选项式 API 略有不同,但核心功能保持一致。
- 基本使用
在 Vue Composition API 中,
watch
函数接收两个参数:要监听的数据源和回调函数。例如,我们有一个响应式数据count
,我们可以这样监听它的变化:
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count has changed from ${oldValue} to ${newValue}`);
});
return {
count
};
}
};
在上述代码中,watch(count, callback)
表示监听 count
的变化,当 count
的值发生改变时,会执行回调函数 callback
,该回调函数接收两个参数:新值 newValue
和旧值 oldValue
。
- 监听复杂数据结构
watch
不仅可以监听简单的ref
数据,还可以监听复杂的数据结构,如对象和数组。例如,我们有一个包含多个属性的对象:
import { reactive, watch } from 'vue';
export default {
setup() {
const user = reactive({
name: 'John',
age: 30
});
watch(user, (newValue, oldValue) => {
console.log('User data has changed');
}, { deep: true });
return {
user
};
}
};
在这个例子中,我们监听了 user
对象的变化。注意,由于对象是引用类型,直接监听对象时,只有当对象的引用发生变化时才会触发回调。如果我们想要监听对象内部属性的变化,需要设置 deep: true
选项。这样,当 user.name
或 user.age
发生变化时,都会触发回调函数。
- 立即执行回调
有时候,我们希望在组件初始化时就执行一次监听回调,而不仅仅是在数据变化时执行。这可以通过设置
immediate: true
选项来实现:
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count has changed from ${oldValue} to ${newValue}`);
}, { immediate: true });
return {
count
};
}
};
在上述代码中,immediate: true
使得回调函数在 count
初始化时就会执行一次,之后 count
发生变化时也会执行。
- 原理剖析
watch
的原理基于 Vue 的响应式系统。当我们使用watch
监听一个数据源时,Vue 会在内部创建一个Watcher
对象。这个Watcher
对象会依赖于数据源的getter
函数。当数据源的值发生变化时,会触发setter
函数,进而通知所有依赖该数据源的Watcher
对象,Watcher
对象会执行我们定义的回调函数。对于复杂数据结构的深度监听,Vue 会递归遍历对象或数组,为每个属性创建依赖,这就是deep
选项的实现原理。
computed 的使用与原理
computed
用于创建计算属性,它的值会基于其他响应式数据进行计算,并且只有在其依赖的数据发生变化时才会重新计算。
- 基本使用
在 Vue Composition API 中,使用
computed
非常简单。例如,我们有两个ref
数据a
和b
,我们想要创建一个计算属性sum
,它的值是a
和b
的和:
import { ref, computed } from 'vue';
export default {
setup() {
const a = ref(1);
const b = ref(2);
const sum = computed(() => {
return a.value + b.value;
});
return {
a,
b,
sum
};
}
};
在上述代码中,computed
接收一个函数,该函数返回计算后的值。我们通过 sum.value
来获取计算属性的值。当 a
或 b
的值发生变化时,sum
会重新计算。
- 计算属性的缓存
computed
最大的特点就是它的缓存机制。计算属性只有在它的依赖值发生变化时才会重新计算。例如,我们在模板中多次使用sum
计算属性:
<template>
<div>
<p>a: {{ a }}</p>
<p>b: {{ b }}</p>
<p>Sum: {{ sum }}</p>
<p>Sum again: {{ sum }}</p>
</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const a = ref(1);
const b = ref(2);
const sum = computed(() => {
console.log('Calculating sum...');
return a.value + b.value;
});
return {
a,
b,
sum
};
}
};
</script>
在上述代码中,当我们第一次访问 sum
时,会输出 Calculating sum...
,表示计算属性进行了计算。但当我们再次访问 sum
时,并不会再次输出 Calculating sum...
,因为 a
和 b
的值没有发生变化,sum
使用了缓存的值。
- 可写的计算属性
通常情况下,计算属性是只读的。但在某些场景下,我们可能需要一个可写的计算属性。这可以通过为
computed
提供一个对象来实现,对象中包含get
和set
函数:
import { ref, computed } from 'vue';
export default {
setup() {
const first = ref(1);
const second = ref(2);
const sum = computed({
get: () => {
return first.value + second.value;
},
set: (newValue) => {
const diff = newValue - (first.value + second.value);
first.value += diff / 2;
second.value += diff / 2;
}
});
return {
first,
second,
sum
};
}
};
在上述代码中,我们定义了一个可写的计算属性 sum
。当我们设置 sum.value
时,会触发 set
函数,在 set
函数中,我们根据新值和旧值的差异来更新 first
和 second
的值。
- 原理剖析
computed
的原理同样基于 Vue 的响应式系统。当我们创建一个计算属性时,Vue 会创建一个ComputedWatcher
对象。这个ComputedWatcher
对象会依赖于计算属性函数中使用的响应式数据的getter
函数。当依赖的数据发生变化时,会标记计算属性为脏,下次访问计算属性时会重新计算值。计算属性的缓存是通过一个标志位来实现的,当计算属性的值没有变化时,会直接返回缓存的值,而不会重新计算。
watch 与 computed 的应用场景对比
-
watch 的应用场景
- 异步操作:当数据变化需要执行异步操作时,
watch
是一个很好的选择。例如,当用户输入搜索关键词时,我们需要根据关键词进行异步搜索并更新搜索结果。
import { ref, watch } from 'vue'; export default { setup() { const searchQuery = ref(''); const searchResults = ref([]); watch(searchQuery, async (newValue) => { if (newValue) { const response = await fetch(`/api/search?query=${newValue}`); const data = await response.json(); searchResults.value = data; } else { searchResults.value = []; } }); return { searchQuery, searchResults }; } };
- 监听复杂数据变化:如前文所述,当需要监听对象或数组内部属性的变化时,
watch
配合deep
选项可以很好地满足需求。例如,在一个购物车应用中,监听购物车中商品列表的变化,包括商品数量、价格等属性的改变,以便实时更新总价等信息。
- 异步操作:当数据变化需要执行异步操作时,
-
computed 的应用场景
- 数据计算与缓存:当需要基于其他响应式数据进行计算,并且希望缓存计算结果以提高性能时,
computed
是首选。例如,在一个电商应用中,计算购物车中商品的总价,由于总价是基于商品列表中的每个商品的价格和数量计算得出,使用computed
可以在商品信息变化时自动更新总价,并且在商品信息未变化时使用缓存的值,避免重复计算。 - 模板中复杂表达式简化:在模板中,如果有复杂的表达式,使用
computed
可以将其提取出来,使模板更加简洁易读。例如,在一个博客应用中,文章的阅读量、点赞数、评论数等数据需要进行一些复杂的计算和格式化后在模板中展示,将这些计算逻辑封装在computed
中,可以使模板代码更清晰。
- 数据计算与缓存:当需要基于其他响应式数据进行计算,并且希望缓存计算结果以提高性能时,
高级应用与注意事项
- 同时监听多个数据源
在某些情况下,我们可能需要同时监听多个数据源的变化。在 Vue Composition API 中,可以通过传递数组作为
watch
的第一个参数来实现:
import { ref, watch } from 'vue';
export default {
setup() {
const a = ref(1);
const b = ref(2);
watch([a, b], (newValues, oldValues) => {
console.log(`a has changed to ${newValues[0]}, b has changed to ${newValues[1]}`);
});
return {
a,
b
};
}
};
在上述代码中,watch([a, b], callback)
表示同时监听 a
和 b
的变化。当 a
或 b
其中任何一个发生变化时,都会触发回调函数,newValues
和 oldValues
是包含 a
和 b
新旧值的数组。
- 在 watch 中使用异步操作的注意事项
当在
watch
的回调函数中使用异步操作时,需要注意回调函数执行的时机。由于异步操作是异步执行的,可能会导致在数据变化后,异步操作还未完成时又发生了新的数据变化。为了解决这个问题,可以使用防抖(debounce)或节流(throttle)技术。例如,使用防抖函数可以确保在一定时间内多次触发数据变化时,只执行一次异步操作:
import { ref, watch } from 'vue';
function debounce(func, delay) {
let timer;
return function() {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(context, args);
}, delay);
};
}
export default {
setup() {
const searchQuery = ref('');
const searchResults = ref([]);
const debouncedSearch = debounce(async (newValue) => {
if (newValue) {
const response = await fetch(`/api/search?query=${newValue}`);
const data = await response.json();
searchResults.value = data;
} else {
searchResults.value = [];
}
}, 300);
watch(searchQuery, debouncedSearch);
return {
searchQuery,
searchResults
};
}
};
在上述代码中,我们定义了一个 debounce
函数,并将其应用在 watch
的回调函数中。这样,当 searchQuery
变化时,只有在停止变化 300 毫秒后才会执行异步搜索操作,避免了频繁触发搜索请求。
- computed 中的依赖优化
在定义
computed
时,要确保计算属性函数中只依赖真正需要的数据。如果依赖了不必要的数据,可能会导致计算属性在不需要重新计算时也进行重新计算,影响性能。例如,在一个用户信息展示组件中,计算属性fullName
只依赖于firstName
和lastName
,而不应该依赖于用户的年龄等无关数据。
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref('John');
const lastName = ref('Doe');
const age = ref(30);
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value;
});
return {
firstName,
lastName,
age,
fullName
};
}
};
在上述代码中,fullName
的计算只依赖于 firstName
和 lastName
,即使 age
发生变化,fullName
也不会重新计算,从而提高了性能。
总结与实践建议
watch
和 computed
是 Vue Composition API 中非常强大的特性,它们在数据处理和响应式系统中发挥着重要作用。在实际开发中,要根据具体的业务需求合理选择使用 watch
还是 computed
。对于需要监听数据变化并执行异步操作或复杂逻辑的场景,优先考虑 watch
;对于需要基于其他响应式数据进行计算并缓存结果的场景,computed
是更好的选择。同时,在使用过程中要注意优化性能,如在 watch
中合理使用防抖、节流技术,在 computed
中确保依赖的准确性。通过深入理解和熟练运用 watch
和 computed
,可以开发出更高效、更灵活的 Vue 应用程序。在实际项目中,多进行实践和总结,不断优化代码结构和性能,以提升用户体验和开发效率。