Vue 3 的 watch 如何实现数据监听、优缺点及详细代码讲解
在 Vue 3 中,watch 是一个非常重要的响应式 API,用于响应式地监听数据的变化,并在变化时执行相应的回调函数。本文将深入探讨 Vue 3 的 watch 是如何实现数据监听的,分析其优缺点,并通过详尽的代码示例帮助大佬们全面理解和应用这一功能。
什么是 watch
watch 是 Vue 3 提供的一个响应式 API,用于观察一个或多个响应式数据源(如 ref 或 reactive 对象)的变化,并在变化时执行一个回调函数。它类似于 Vue 2 中的 watch 选项,但在 Composition API 中得到了更广泛的应用和增强。
为什么需要 watch
在 Vue 的响应式系统中,computed 通常用于基于响应式数据生成新的派生数据。然而,当需要在数据变化时执行一些副作用(如异步操作、手动 DOM 操作、调用 API 等)时,computed 不再适用。这时,watch 就成为了一个理想的选择。
watch 的内部实现原理
要理解 watch 的实现机制,必须先了解 Vue 3 的响应式系统以及依赖收集与触发的基本原理。
响应式系统概述
Vue 3 的响应式系统基于 ES6 的 Proxy 实现,能够高效地追踪数据的变化。核心概念包括:
- 响应式对象:通过
reactive或ref创建的对象,实现了数据的自动追踪。 - 依赖收集:当组件渲染或函数执行时,系统会收集访问的数据作为依赖。
- 依赖触发:当数据变化时,系统会触发相应的依赖,更新视图或执行回调。
依赖收集与触发
在 Vue 3 中,依赖收集和触发由一个全局的 effect 函数栈管理。基本流程如下:
- 依赖收集:当响应式数据被访问时,将当前的
effect函数添加到该数据的依赖列表中。 - 依赖触发:当响应式数据变化时,遍历其依赖列表,重新执行这些
effect函数。
watch 的实现机制
watch 的实现机制基于 Vue 3 的响应式系统和 effect 函数。其基本流程如下:
- 初始化:
watch接收一个或多个数据源和一个回调函数。 - 创建 Effect:为回调函数创建一个新的
effect,并在执行时追踪数据源的依赖。 - 触发回调:当数据源变化时,
effect被重新执行,调用回调函数并传递新值和旧值。
watch 的使用方法
watch 在 Vue 3 的 Composition API 中被广泛使用,以下是其主要的使用方法和选项。
基本用法
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`);
});
return { count };
},
};
监听多个来源
import { ref, watch } from 'vue';
export default {
setup() {
const firstName = ref('John');
const lastName = ref('Doe');
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`);
});
return { firstName, lastName };
},
};
深度监听
import { reactive, watch } from 'vue';
export default {
setup() {
const user = reactive({
name: 'John',
address: {
city: 'New York',
},
});
watch(
() => user,
(newUser, oldUser) => {
console.log('User object changed:', newUser);
},
{ deep: true }
);
return { user };
},
};
立即执行
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
watch(
count,
(newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`);
},
{ immediate: true }
);
return { count };
},
};
清理副作用
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
watch(count, (newVal, oldVal, onCleanup) => {
const timer = setInterval(() => {
console.log('Count:', count.value);
}, 1000);
onCleanup(() => {
clearInterval(timer);
});
});
return { count };
},
};
watch 的优缺点
优点
- 副作用管理:
watch能够在数据变化时执行副作用,如异步操作、手动 DOM 操作等。 - 灵活性高:支持监听单个或多个数据源,可以进行深度监听和立即执行等配置。
- 组合性强:与 Composition API 无缝集成,便于逻辑复用和代码组织。
- 类型支持:在 TypeScript 中,
watch提供了良好的类型推导支持。
缺点
- 复杂性:相比
computed,watch需要更多的配置选项,可能增加代码复杂度。 - 性能考虑:深度监听和监听多个数据源时,可能带来一定的性能开销。
- 依赖管理:不当使用
watch可能导致依赖关系混乱,影响代码的可维护性。
详细代码讲解
为了更好地理解 watch 的工作原理和使用方法,以下将通过多个详细的代码示例进行说明。
示例 1:简单的 watch 使用
文件结构
watch-demo/
├── index.html
├── App.vue
└── main.js
index.html
<!-- watch-demo/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Vue 3 Watch 示例 - 简单使用</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
main.js
// watch-demo/main.js
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
App.vue
<!-- watch-demo/App.vue -->
<template>
<div>
<h1>Vue 3 Watch 示例 - 简单使用</h1>
<p>当前计数: {{ count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
// 定义一个watch监听count的变化
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变为${newVal}`);
});
// 增加计数的方法
const increment = () => {
count.value++;
};
</script>
<style scoped>
h1 {
color: #42b983;
}
button {
padding: 10px 20px;
font-size: 16px;
}
</style>
运行效果
点击“增加计数”按钮,count 的值会增加,并在控制台中打印出变化信息。
示例 2:监听多个数据来源
App.vue
<!-- watch-demo/App.vue -->
<template>
<div>
<h1>Vue 3 Watch 示例 - 监听多个数据来源</h1>
<div>
<label>名字: </label>
<input v-model="firstName" placeholder="First Name" />
</div>
<div>
<label>姓氏: </label>
<input v-model="lastName" placeholder="Last Name" />
</div>
<p>全名: {{ fullName }}</p>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// 监听多个数据来源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`名字从${oldFirst} ${oldLast}变为${newFirst} ${newLast}`);
});
</script>
<style scoped>
h1 {
color: #42b983;
}
div {
margin-bottom: 10px;
}
label {
margin-right: 10px;
}
input {
padding: 5px;
font-size: 14px;
}
</style>
运行效果
在名字和姓氏输入框中输入内容,控制台会打印出完整名字的变化。
示例 3:深度监听对象
App.vue
<!-- watch-demo/App.vue -->
<template>
<div>
<h1>Vue 3 Watch 示例 - 深度监听对象</h1>
<div>
<label>用户名: </label>
<input v-model="user.name" placeholder="Name" />
</div>
<div>
<label>城市: </label>
<input v-model="user.address.city" placeholder="City" />
</div>
<p>用户信息:</p>
<pre>{{ user }}</pre>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue';
const user = reactive({
name: 'John',
address: {
city: 'New York',
},
});
// 深度监听user对象的变化
watch(
() => user,
(newUser, oldUser) => {
console.log('用户信息发生变化:', newUser, oldUser);
},
{ deep: true }
);
</script>
<style scoped>
h1 {
color: #42b983;
}
div {
margin-bottom: 10px;
}
label {
margin-right: 10px;
}
input {
padding: 5px;
font-size: 14px;
}
pre {
background-color: #f0f0f0;
padding: 10px;
}
</style>
运行效果
修改用户名或城市,控制台会打印出用户对象的新旧值。
示例 4:立即执行回调函数
App.vue
<!-- watch-demo/App.vue -->
<template>
<div>
<h1>Vue 3 Watch 示例 - 立即执行回调函数</h1>
<div>
<label>输入框: </label>
<input v-model="inputValue" placeholder="输入内容" />
</div>
<p>输入内容: {{ inputValue }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const inputValue = ref('初始值');
// 监听inputValue,并立即执行回调
watch(
inputValue,
(newVal, oldVal) => {
console.log(`inputValue从${oldVal}变为${newVal}`);
},
{ immediate: true }
);
</script>
<style scoped>
h1 {
color: #42b983;
}
div {
margin-bottom: 10px;
}
label {
margin-right: 10px;
}
input {
padding: 5px;
font-size: 14px;
}
</style>
运行效果
页面加载时,watch 的回调会立即执行,打印出初始值的信息。之后,修改输入框内容时,回调也会随之执行。
示例 5:清理副作用
App.vue
<!-- watch-demo/App.vue -->
<template>
<div>
<h1>Vue 3 Watch 示例 - 清理副作用</h1>
<button @click="start">开始计时</button>
<button @click="stop">停止计时</button>
<p>计时器状态: {{ isRunning ? '运行中' : '已停止' }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const isRunning = ref(false);
let timer = null;
// 监听isRunning的变化
watch(
isRunning,
(newVal, oldVal, onCleanup) => {
if (newVal) {
// 开始计时
timer = setInterval(() => {
console.log('计时中...');
}, 1000);
// 清理函数,停止计时
onCleanup(() => {
clearInterval(timer);
console.log('计时已停止。');
});
}
}
);
// 开始计时
const start = () => {
isRunning.value = true;
};
// 停止计时
const stop = () => {
isRunning.value = false;
};
</script>
<style scoped>
h1 {
color: #42b983;
}
button {
padding: 10px 20px;
margin-right: 10px;
font-size: 16px;
}
p {
margin-top: 20px;
font-size: 18px;
}
</style>
运行效果
点击“开始计时”按钮,控制台每秒打印一次“计时中...”。点击“停止计时”按钮,计时停止,并在控制台打印“计时已停止。”。
示例 6:结合 watchEffect 使用
App.vue
<!-- watch-demo/App.vue -->
<template>
<div>
<h1>Vue 3 Watch 示例 - 结合 watchEffect 使用</h1>
<div>
<label>输入A: </label>
<input v-model="a" placeholder="输入A" />
</div>
<div>
<label>输入B: </label>
<input v-model="b" placeholder="输入B" />
</div>
<p>计算结果: {{ a + b }}</p>
</div>
</template>
<script setup>
import { ref, watch, watchEffect } from 'vue';
const a = ref(0);
const b = ref(0);
// 使用 watch 监听a的变化
watch(a, (newVal, oldVal) => {
console.log(`a从${oldVal}变为${newVal}`);
});
// 使用 watchEffect 自动收集依赖
watchEffect(() => {
console.log(`watchEffect: a + b = ${a.value + b.value}`);
});
</script>
<style scoped>
h1 {
color: #42b983;
}
div {
margin-bottom: 10px;
}
label {
margin-right: 10px;
}
input {
padding: 5px;
font-size: 14px;
width: 200px;
}
p {
font-size: 18px;
}
</style>
运行效果
输入框A和B的内容变化时,控制台会分别打印watch和watchEffect的相关信息。
示例 7:自定义 watch 实现原理简述
为了更深入地理解 watch 的实现原理,可以通过一个简化版的自定义 watch 函数来模拟其工作机制。
App.vue
<!-- watch-demo/App.vue -->
<template>
<div>
<h1>Vue 3 Watch 示例 - 自定义 watch 实现原理</h1>
<p>计数: {{ count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
// 简化版的 Dep 类,用于依赖收集和触发
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (Dep.target) {
this.subscribers.add(Dep.target);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
// 全局 target,用于保存当前正在收集依赖的函数
Dep.target = null;
// 创建一个响应式对象
function reactiveObject(obj) {
const depsMap = new Map();
return new Proxy(obj, {
get(target, key) {
if (!depsMap.has(key)) {
depsMap.set(key, new Dep());
}
const dep = depsMap.get(key);
dep.depend();
return target[key];
},
set(target, key, value) {
target[key] = value;
if (depsMap.has(key)) {
const dep = depsMap.get(key);
dep.notify();
}
return true;
},
});
}
// 简化版的 watch 实现
function customWatch(source, cb) {
let getter;
if (typeof source === 'function') {
getter = source;
} else {
getter = () => source.value;
}
let oldValue;
const update = () => {
const newValue = getter();
cb(newValue, oldValue);
oldValue = newValue;
};
Dep.target = update;
oldValue = getter();
Dep.target = null;
}
// 使用自定义 watch
const state = reactiveObject({
count: 0,
});
customWatch(
() => state.count,
(newVal, oldVal) => {
console.log(`customWatch: count从${oldVal}变为${newVal}`);
}
);
// 增加计数的方法
const increment = () => {
state.count++;
};
</script>
<style scoped>
h1 {
color: #42b983;
}
button {
padding: 10px 20px;
font-size: 16px;
}
p {
font-size: 18px;
}
</style>
运行效果
点击“增加计数”按钮,控制台会打印出自定义 watch 函数的变化信息,模拟了 Vue 3 watch 的基本工作流程。
说明:
- Dep 类:用于管理依赖。每个响应式属性对应一个
Dep实例,存储订阅者(响应式函数)。 - reactiveObject 函数:创建一个简化版的响应式对象,使用
Proxy进行拦截,并在get和set操作时进行依赖收集和触发。 - customWatch 函数:模拟
watch的基本功能,接受一个数据源和回调函数。当数据源变化时,执行回调。 - 使用示例:创建一个响应式状态对象
state,通过customWatch监听state.count的变化,并在变化时打印信息。
此示例展示了 watch 的基本原理,包括依赖收集和变化触发,但省略了许多 Vue 3 中的复杂优化和功能。
总结
watch 是 Vue 3 中用于监听数据变化并执行副作用的强大工具。它在 Composition API 中得到了广泛应用,提供了灵活的配置选项和良好的类型支持。通过理解 watch 的内部实现原理和实际应用场景,开发者可以更高效地管理组件中的副作用,提高代码的可维护性和扩展性。
关键点总结:
- 响应式系统:
watch基于 Vue 3 的响应式系统,通过依赖收集和触发机制实现数据监听。 - 使用方法:支持监听单个或多个数据源,提供深度监听和立即执行等选项。
- 优缺点:
watch提供了灵活的副作用管理,但需要注意配置选项和潜在的性能开销。 - 实战应用:通过多个详细的代码示例,展示了
watch在不同场景下的使用方法和效果。
掌握 watch 的使用,能够帮助开发者更好地处理复杂的数据依赖和副作用需求,提升 Vue 应用的整体性能和用户体验。