大家好,我是你们的老朋友,今天咱们来聊聊Vue 3里一对形影不离的好兄弟:watch
和 watchEffect
。 别看它们名字有点像,功能也都是监听数据变化,但内部的运行机制和使用场景可是大有不同。 搞清楚这些差异,能让你在Vue的世界里更加游刃有余,写出更高效、更优雅的代码。
开场白:侦察兵与预言家
如果把数据变化比作战场上的风吹草动,watch
就像一位经验丰富的侦察兵,你需要明确告诉他要监视哪个目标,他才会时刻关注,并汇报目标的最新动向。 而watchEffect
则更像一位拥有预知能力的预言家,他会主动感知周围环境的变化,并根据这些变化做出相应的反应,无需你明确指定监视目标。
第一部分:watch
– 精准打击的侦察兵
watch
允许你监听一个或多个数据源,并在数据源发生变化时执行回调函数。 它可以监听的类型非常广泛:
- 响应式数据:
ref
、reactive
创建的数据。 - getter 函数: 返回值的函数。
- 多个数据源: 以数组形式传入。
1. 基本用法:
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
// 监听 count 的变化
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变成了 ${newValue}`);
});
const increment = () => {
count.value++;
};
return {
count,
increment
};
}
};
在这个例子中,我们使用 watch
监听了 count
这个 ref
变量的变化。 当 count
的值发生改变时,回调函数就会被执行,并打印出新旧值。
2. 监听多个数据源:
import { ref, reactive, watch } from 'vue';
export default {
setup() {
const firstName = ref('张');
const lastName = ref('三');
const fullName = ref('');
// 监听 firstName 和 lastName 的变化
watch([firstName, lastName], ([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
fullName.value = newFirstName + newLastName;
console.log(`姓名从 ${oldFirstName + oldLastName} 变成了 ${newFirstName + newLastName}`);
}, { immediate: true }); // 立即执行一次
return {
firstName,
lastName,
fullName
};
}
};
这里,我们使用数组形式传入了 firstName
和 lastName
两个 ref
变量。 当其中任何一个变量发生变化时,回调函数都会被执行。 回调函数的第一个参数是一个数组,包含了新的 firstName
和 lastName
的值;第二个参数也是一个数组,包含了旧的 firstName
和 lastName
的值。
3. 监听 reactive 对象中的属性:
import { reactive, watch } from 'vue';
export default {
setup() {
const person = reactive({
name: '李四',
age: 20
});
// 监听 person.age 的变化
watch(() => person.age, (newValue, oldValue) => {
console.log(`年龄从 ${oldValue} 变成了 ${newValue}`);
});
const growUp = () => {
person.age++;
};
return {
person,
growUp
};
}
};
注意,这里我们使用了一个 getter 函数 () => person.age
来监听 person
对象的 age
属性。 直接传入 person.age
是不行的! 因为这样传递的是一个原始值,而不是响应式引用,watch无法追踪到变化。 getter 函数确保了我们监听的是响应式依赖。
4. watch
的选项:
watch
还可以接受一个可选的配置对象,用于控制其行为:
选项 | 类型 | 描述 |
---|---|---|
immediate |
boolean |
是否在组件挂载后立即执行回调函数。 默认值为 false 。 |
deep |
boolean |
是否深度监听对象内部的变化。 默认值为 false 。 |
flush |
'pre' | 'post' | 'sync' |
指定回调函数的刷新时机。 pre (默认): 在组件更新之前执行。 post : 在组件更新之后执行。 sync :同步执行。 (通常不建议使用sync ,可能导致性能问题) |
onTrack |
Function |
用于调试侦听器的依赖项跟踪。 |
onTrigger |
Function |
用于调试侦听器的依赖项触发。 |
flush: 'sync' |
string |
同步的刷新时机。 极少使用, 可能会阻塞UI。 |
-
immediate: true
: 让watch
在组件初始化时立即执行一次回调函数。 这在某些需要根据初始值进行一些操作的场景下非常有用。 -
deep: true
: 用于深度监听对象内部的变化。 默认情况下,watch
只会监听对象的引用是否发生变化,而不会监听对象内部属性的变化。 如果需要监听对象内部属性的变化,就需要将deep
设置为true
。 请谨慎使用deep: true
,因为它会带来性能上的开销。
import { reactive, watch } from 'vue';
export default {
setup() {
const person = reactive({
address: {
city: '北京'
}
});
// 深度监听 person.address.city 的变化
watch(() => person.address.city, (newValue, oldValue) => {
console.log(`城市从 ${oldValue} 变成了 ${newValue}`);
}, { deep: true }); // 加上 deep: true 才能监听到
const changeCity = () => {
person.address.city = '上海';
};
return {
person,
changeCity
};
}
};
在这个例子中,我们使用了 deep: true
来深度监听 person.address.city
的变化。 如果没有 deep: true
,watch
将不会监听到 city
的变化。
flush: 'post'
: 默认情况下,watch
的回调函数会在组件更新之前执行。 如果你需要在组件更新之后执行回调函数,可以将flush
设置为'post'
。 这在某些需要访问更新后的 DOM 的场景下非常有用。
第二部分:watchEffect
– 自我感知的预言家
watchEffect
会立即执行一次回调函数,并在回调函数执行过程中自动追踪所有响应式依赖。 当这些依赖发生变化时,回调函数会自动重新执行。
1. 基本用法:
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
watchEffect(() => {
console.log(`count 的值为:${count.value}`);
});
const increment = () => {
count.value++;
};
return {
count,
increment
};
}
};
在这个例子中,watchEffect
会立即执行一次回调函数,并在回调函数执行过程中追踪到 count
这个 ref
变量。 当 count
的值发生改变时,回调函数会自动重新执行。 注意,watchEffect
不需要你手动指定监听哪个数据源,它会自动追踪回调函数中用到的所有响应式数据。
2. 停止监听:
watchEffect
会返回一个停止函数,你可以调用这个函数来停止监听。
import { ref, watchEffect, onUnmounted } from 'vue';
export default {
setup() {
const count = ref(0);
const stop = watchEffect(() => {
console.log(`count 的值为:${count.value}`);
});
const increment = () => {
count.value++;
};
// 在组件卸载时停止监听
onUnmounted(() => {
stop();
});
return {
count,
increment
};
}
};
在这个例子中,我们使用 onUnmounted
生命周期钩子函数在组件卸载时调用 stop
函数来停止监听。 记住,一定要在组件卸载时停止监听,否则可能会导致内存泄漏。
3. watchEffect
的选项:
watchEffect
也可以接受一个可选的配置对象,用于控制其行为:
选项 | 类型 | 描述 |
---|---|---|
flush |
'pre' | 'post' | 'sync' |
指定回调函数的刷新时机。 pre (默认): 在组件更新之前执行。 post : 在组件更新之后执行。 sync :同步执行。 (通常不建议使用sync ,可能导致性能问题) |
onTrack |
Function |
用于调试侦听器的依赖项跟踪。 |
onTrigger |
Function |
用于调试侦听器的依赖项触发。 |
flush
、onTrack
、onTrigger
的作用与 watch
中的相同。
4.清除副作用
watchEffect
的回调函数可以返回一个清除函数。这个清除函数会在下次回调函数执行之前被调用。这对于清除副作用非常有用,例如取消定时器、移除事件监听器等。
import { ref, watchEffect, onUnmounted } from 'vue';
export default {
setup() {
const count = ref(0);
watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log(`count 的值为:${count.value}`);
}, 1000);
// 在下次回调函数执行之前清除定时器
onInvalidate(() => {
clearTimeout(timer);
});
});
const increment = () => {
count.value++;
};
return {
count,
increment
};
}
};
在这个例子中,我们在 watchEffect
的回调函数中设置了一个定时器。 为了避免定时器重复执行,我们在 onInvalidate
函数中清除了定时器。 onInvalidate
函数会在下次回调函数执行之前被调用。
第三部分:watch
vs watchEffect
– 侦察兵与预言家的选择
特性 | watch |
watchEffect |
---|---|---|
监听目标 | 需要明确指定要监听的数据源。 | 自动追踪回调函数中用到的所有响应式数据。 |
执行时机 | 只有在监听的数据源发生变化时才会执行回调函数。 | 立即执行一次回调函数,并在依赖发生变化时自动重新执行。 |
依赖追踪 | 手动指定依赖。 | 自动追踪依赖。 |
停止监听 | 返回值不是停止函数,需要手动管理停止。 | 返回一个停止函数,用于停止监听。 |
清除副作用 | 依赖变化后才执行回调。 如果需要清除副作用,需要在回调函数内部手动处理。 | 可返回清除函数,在下次回调函数执行之前执行,用于清除副作用。 |
使用场景 | 需要精确控制监听目标,并且只需要在特定数据源发生变化时才执行回调函数的场景。 | 需要根据多个数据源的变化来执行一些操作,并且希望自动追踪依赖的场景。 |
适用场景举例 | 1. 监听单个或多个特定属性的变化,例如监听用户名的变化并更新欢迎信息。 2. 监听路由的变化并加载不同的组件。 3. 基于一些条件判断是否执行副作用。 | 1. 根据多个响应式数据计算出一个新的值。 2. 在组件初始化时执行一些副作用,例如发送网络请求、设置定时器等。 3. 根据多个响应式数据的变化来更新 DOM。 |
性能考量 | 由于需要手动指定监听目标,因此可以更精确地控制监听范围,避免不必要的性能开销。 | 由于会自动追踪依赖,因此可能会追踪到一些不必要的依赖,导致性能开销增加。 |
初次执行 | 只有当监听的属性发生变化时才会执行回调函数(除非设置了immediate: true )。 |
会在组件挂载后立即执行一次回调函数。 |
显式/隐式依赖 | 依赖关系是显式声明的,通过传递 ref 或 reactive 属性给 watch 。 |
依赖关系是隐式的,通过在 watchEffect 回调函数中访问响应式属性来建立。 |
形象的比喻:
watch
: 就像一个订阅服务,你需要告诉它你对哪些信息感兴趣,它才会给你发送相关的通知。watchEffect
: 就像一个智能家居系统,它会自动感知房间里的温度、湿度、光线等变化,并根据这些变化自动调节空调、加湿器、灯光等设备。
总结:
watch
和 watchEffect
都是 Vue 3 中非常重要的 API,它们可以帮助你监听数据变化并执行相应的操作。 选择哪个 API 取决于你的具体需求。 如果你需要精确控制监听目标,并且只需要在特定数据源发生变化时才执行回调函数,那么 watch
是一个不错的选择。 如果你需要根据多个数据源的变化来执行一些操作,并且希望自动追踪依赖,那么 watchEffect
可能会更方便。 记住,没有绝对的好与坏,只有适合与不适合。
第四部分:代码示例:更贴近实战
场景 1:监听用户输入,进行搜索
<template>
<input type="text" v-model="searchText">
<ul>
<li v-for="item in searchResults" :key="item">{{ item }}</li>
</ul>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const searchText = ref('');
const searchResults = ref([]);
// 模拟搜索函数
const performSearch = async (text) => {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟搜索结果
return Array.from({ length: 5 }, (_, i) => `${text}-Result-${i + 1}`);
};
// 监听 searchText 的变化
watch(searchText, async (newText) => {
if (newText.length > 0) {
searchResults.value = await performSearch(newText);
} else {
searchResults.value = [];
}
});
return {
searchText,
searchResults
};
}
};
</script>
在这个例子中,我们使用 watch
监听了 searchText
的变化。 当用户输入时,searchText
的值会发生改变,watch
的回调函数会被执行,并调用 performSearch
函数进行搜索。
场景 2:根据窗口大小调整布局
<template>
<div>
<p>当前布局:{{ layout }}</p>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const layout = ref('desktop');
watchEffect(() => {
if (window.innerWidth < 768) {
layout.value = 'mobile';
} else {
layout.value = 'desktop';
}
});
return {
layout
};
}
};
</script>
在这个例子中,我们使用 watchEffect
监听了窗口大小的变化。 当窗口大小发生改变时,watchEffect
的回调函数会被执行,并根据窗口大小调整 layout
的值。
场景 3:根据用户角色显示不同的权限
<template>
<div>
<p v-if="hasAdminPermission">管理员权限</p>
<p v-if="hasEditorPermission">编辑权限</p>
<p v-if="hasViewerPermission">查看权限</p>
</div>
</template>
<script>
import { ref, reactive, computed, watchEffect } from 'vue';
export default {
setup() {
const user = reactive({
role: 'viewer' // 或者 'editor', 'admin'
});
const hasAdminPermission = computed(() => user.role === 'admin');
const hasEditorPermission = computed(() => user.role === 'editor' || user.role === 'admin');
const hasViewerPermission = computed(() => user.role === 'viewer' || user.role === 'editor' || user.role === 'admin');
// 模拟用户角色变化
const changeRole = (newRole) => {
user.role = newRole;
};
return {
hasAdminPermission,
hasEditorPermission,
hasViewerPermission,
changeRole
};
}
};
</script>
在这个例子中,我们使用了computed
来计算用户权限,并且使用了watchEffect
来响应用户角色变化。虽然这里用watchEffect
也能实现,但更好的方式是使用computed
,因为权限本质上是基于用户角色的派生数据,computed
更适合描述这种关系。 如果用watchEffect
,每当user.role
变化,watchEffect
会重新运行,虽然也能更新权限,但不如computed
高效和语义化。
总结:
希望通过今天的讲解,大家对 Vue 3 中的 watch
和 watchEffect
有了更深入的了解。 在实际开发中,要根据具体场景选择合适的 API,并注意性能优化,写出更高效、更优雅的代码。 记住,熟练掌握这些基础知识,才能在 Vue 的世界里自由驰骋!
今天的讲座就到这里,谢谢大家! 祝大家编程愉快,Bug 少少!