Vue3 引入了组合式 API,其中 watchEffect
和 watch
是两个非常重要的响应式 API,用于在响应式数据发生变化时执行特定的副作用。本文将详细探讨 watchEffect
和 watch
的实现原理、在项目中的应用、实际使用技巧以及如何手写类似功能的代码。
watchEffect
与 watch
简介
watchEffect
watchEffect
是一个立即执行的副作用函数,当其依赖的响应式数据变化时,副作用函数会重新运行。它类似于 Vue2 中的 computed
和 watch
的结合,但使用更简单。
watch
watch
则更为灵活,它允许显式地指定依赖的响应式数据,并提供了更多的选项来控制副作用的执行时机和方式。
官方文档解读
为了更好地理解这两个 API,我们首先来看看官方文档的描述:
- watchEffect
- watch
官方文档详细介绍了这两个 API 的用法和参数配置。watchEffect
更适合简单的副作用逻辑,而 watch
则适用于需要精细控制依赖变化和执行逻辑的场景。
watchEffect
与 watch
的实现原理
watchEffect
的实现原理
watchEffect
的核心在于其依赖自动收集机制。当副作用函数执行时,Vue 会自动追踪其中访问的响应式数据,并在这些数据变化时重新执行副作用函数。
实现步骤:
- 依赖收集:当副作用函数访问响应式数据时,将其注册到依赖集合中。
- 触发更新:当响应式数据变化时,依赖集合中的副作用函数会被重新执行。
代码示例:
function watchEffect(effect) {
// 用于存储依赖的副作用函数
const deps = new Set();
// 包装 effect 函数以便重新运行
const runner = () => {
// 清空之前的依赖
deps.clear();
// 运行副作用函数,并记录新依赖
effect();
};
// 模拟响应式依赖追踪
// 每次获取响应式数据时,注册依赖
const reactiveHandler = {
get(target, key, receiver) {
deps.add(runner);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 触发依赖的副作用函数重新执行
deps.forEach(dep => dep());
return result;
}
};
// 创建响应式对象的代理
const reactive = (obj) => new Proxy(obj, reactiveHandler);
// 初始化时运行副作用函数
runner();
return reactive;
}
watch
的实现原理
watch
的实现更加复杂,允许对依赖数据的变化进行细粒度控制。它通过比较新旧值来决定是否触发回调函数。
实现步骤:
- 依赖收集:同样通过依赖追踪机制来收集响应式数据。
- 新旧值比较:在数据变化时,通过比较新旧值来决定是否执行回调。
- 回调执行:根据配置选项执行回调函数。
代码示例:
function watch(source, callback, options = {}) {
let oldValue, newValue;
// 存储依赖的回调函数
const deps = new Set();
// 包装回调函数以便执行和记录新旧值
const runner = () => {
newValue = source();
if (newValue !== oldValue || options.deep) {
callback(newValue, oldValue);
oldValue = newValue;
}
};
// 模拟响应式依赖追踪
// 每次获取响应式数据时,注册依赖
const reactiveHandler = {
get(target, key, receiver) {
deps.add(runner);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 触发依赖的回调函数重新执行
deps.forEach(dep => dep());
return result;
}
};
// 创建响应式对象的代理
const reactive = (obj) => new Proxy(obj, reactiveHandler);
// 根据选项决定是否立即执行回调函数
if (options.immediate) {
runner();
} else {
oldValue = source();
}
return reactive;
}
项目中的实际应用
数据同步
在表单输入与后台数据同步时,watchEffect
可以简化实现:
import { ref, watchEffect } from 'vue';
const data = ref('');
watchEffect(() => {
console.log(`数据变化:${data.value}`);
});
复杂逻辑处理
在需要复杂逻辑处理时,使用 watch
更加合适:
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`count 变化:从 ${oldValue} 到 ${newValue}`);
}, { immediate: true });
性能优化
在大型应用中,合理使用 watch
和 watchEffect
可以提升性能。例如,通过控制依赖收集的粒度,减少不必要的计算和 DOM 更新。
调试和日志
watch
和 watchEffect
也可以用于调试和日志记录。在开发阶段,通过打印响应式数据的变化,可以更直观地了解数据流动和应用状态。
异步操作
在处理异步操作时,watch
提供了更多的灵活性。例如,可以在回调函数中处理 API 请求,并根据响应数据更新视图。
import { ref, watch } from 'vue';
const userId = ref(1);
const userData = ref(null);
watch(userId, async (newId) => {
userData.value = await fetchUserData(newId);
}, { immediate: true });
async function fetchUserData(id) {
const response = await fetch(`https:///users/${id}`);
return response.json();
}
手写 watchEffect
与 watch
的实现
手写 watchEffect
下面是一个更详细的 watchEffect
实现,加入了更多细节来展示其核心原理。
function watchEffect(effect) {
// 用于存储依赖的副作用函数
const deps = new Set();
// 包装 effect 函数以便重新运行
const runner = () => {
// 清空之前的依赖
deps.clear();
// 运行副作用函数,并记录新依赖
effect();
};
// 模拟响应式依赖追踪
// 每次获取响应式数据时,注册依赖
const reactiveHandler = {
get(target, key, receiver) {
deps.add(runner);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 触发依赖的副作用函数重新执行
deps.forEach(dep => dep());
return result;
}
};
// 创建响应式对象的
代理
const reactive = (obj) => new Proxy(obj, reactiveHandler);
// 初始化时运行副作用函数
runner();
return reactive;
}
// 使用示例
const state = watchEffect(() => {
console.log(state.value);
});
state.value = 1; // 触发副作用函数
state.value = 2; // 再次触发副作用函数
手写 watch
下面是一个更详细的 watch
实现,包含更多的实现细节和选项处理。
function watch(source, callback, options = {}) {
let oldValue, newValue;
// 存储依赖的回调函数
const deps = new Set();
// 包装回调函数以便执行和记录新旧值
const runner = () => {
newValue = source();
if (newValue !== oldValue || options.deep) {
callback(newValue, oldValue);
oldValue = newValue;
}
};
// 模拟响应式依赖追踪
// 每次获取响应式数据时,注册依赖
const reactiveHandler = {
get(target, key, receiver) {
deps.add(runner);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 触发依赖的回调函数重新执行
deps.forEach(dep => dep());
return result;
}
};
// 创建响应式对象的代理
const reactive = (obj) => new Proxy(obj, reactiveHandler);
// 根据选项决定是否立即执行回调函数
if (options.immediate) {
runner();
} else {
oldValue = source();
}
return reactive;
}
// 使用示例
const count = watch(
() => count.value,
(newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
},
{ immediate: true }
);
count.value = 1; // 触发回调
count.value = 2; // 再次触发回调
实际项目中的使用技巧
避免无限循环
在使用 watchEffect
或 watch
时,需要特别注意避免无限循环。例如,当副作用函数中直接修改响应式数据时,可能会导致无限循环。为了避免这种情况,可以在副作用中使用条件判断来控制数据更新。
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
if (count.value < 10) {
count.value++;
}
});
合理设置选项
watch
提供了多种选项来控制回调的执行时机和方式。例如,可以通过设置 immediate
选项让回调在初始化时立即执行,通过 deep
选项实现对嵌套对象的深度监听。
import { ref, watch } from 'vue';
const user = ref({ name: 'John', age: 30 });
watch(user, (newValue, oldValue) => {
console.log('User data changed:', newValue);
}, { deep: true, immediate: true });
结合其他 API 使用
在实际项目中,watchEffect
和 watch
常常需要与其他 Vue API 结合使用。例如,可以与 computed
结合,实现复杂的计算逻辑和副作用处理。
import { ref, computed, watchEffect } from 'vue';
const num1 = ref(1);
const num2 = ref(2);
const sum = computed(() => num1.value + num2.value);
watchEffect(() => {
console.log(`Sum: ${sum.value}`);
});
处理复杂数据结构
当处理复杂的数据结构时,可以使用 watch
的 deep
选项实现深度监听。此外,还可以使用自定义的比较函数来优化性能。
import { ref, watch } from 'vue';
const data = ref({
user: {
name: 'John',
address: {
city: 'New York',
zip: '10001'
}
}
});
watch(() => data.value.user, (newValue, oldValue) => {
console.log('User data changed:', newValue);
}, { deep: true });
更多实际应用案例
表单验证
在表单验证中,watchEffect
和 watch
可以用于实时验证用户输入。例如:
import { ref, watch } from 'vue';
const username = ref('');
const usernameError = ref('');
watch(username, (newValue) => {
if (newValue.length < 3) {
usernameError.value = '用户名长度必须大于 3';
} else {
usernameError.value = '';
}
});
数据持久化
在应用中,需要将数据持久化到本地存储时,可以使用 watch
监控数据变化并进行保存:
import { ref, watch } from 'vue';
const settings = ref({
theme: 'dark',
notifications: true
});
watch(settings, (newValue) => {
localStorage.setItem('settings', JSON.stringify(newValue));
}, { deep: true });
// 初始化时从本地存储读取
const storedSettings = JSON.parse(localStorage.getItem('settings'));
if (storedSettings) {
settings.value = storedSettings;
}
组件间通信
在父子组件通信时,可以使用 watch
监听父组件传递的 props 并进行处理:
父组件:
<template>
<ChildComponent :data="parentData" />
</template>
<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
setup() {
const parentData = ref('父组件数据');
return { parentData };
}
};
</script>
子组件:
<template>
<div>{{ processedData }}</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
props: ['data'],
setup(props) {
const processedData = ref('');
watch(() => props.data, (newData) => {
processedData.value = newData + ' - 已处理';
});
return { processedData };
}
};
</script>
动画效果
在处理动画效果时,可以使用 watchEffect
动态监听数据变化并触发动画:
import { ref, watchEffect } from 'vue';
const isVisible = ref(false);
watchEffect(() => {
if (isVisible.value) {
document.getElementById('animatedElement').classList.add('fade-in');
} else {
document.getElementById('animatedElement').classList.remove('fade-in');
}
});
// 在模板中绑定 isVisible 来控制动画效果
<template>
<div id="animatedElement" :class="{ 'fade-in': isVisible }">动画元素</div>
<button @click="isVisible = !isVisible">切换动画</button>
</template>
总结
watchEffect
和 watch
是 Vue3 中两个非常强大的响应式 API,它们分别适用于简单和复杂的副作用处理。在项目中合理使用这两个 API,可以极大地提升代码的可维护性和性能。通过手写简化版的 watchEffect
和 watch
实现,我们更加深入理解了其背后的实现原理。本文还介绍了在实际项目中使用这两个 API 的技巧和注意事项,帮助开发者更好地掌握和应用它们。
希望这篇博客能够帮助你深入了解 watchEffect
和 watch
的实现原理和应用场景。