Vue 3 的响应式系统是其核心特性之一,effectScope
、watchEffect
、effect
和 watch
是响应式 API 的重要组成部分。它们在管理响应式副作用和数据监听方面提供了强大的功能。本文将深入探讨这四个 API 的定义、使用场景、代码示例及注意事项,并特别详细讲解 watchEffect
中 flush
选项(pre
、post
和 sync
),而对 watch
的 flush
进行简要说明,帮助开发者更好地理解和使用它们。
一、什么是 effect?
effect
是 Vue 3 响应式系统的核心函数,位于 @vue/reactivity
包中。它用于创建一个响应式副作用,当依赖的响应式数据发生变化时,副作用会自动重新执行。effect
是 watchEffect
、watch
和其他响应式 API 的底层实现。
1.1 effect 的基本用法
effect
接受一个函数作为参数,这个函数会在创建时立即执行,并自动收集其中的响应式依赖。当依赖变化时,函数会重新运行。
javascript
import { reactive, effect } from 'vue';
const state = reactive({ count: 0 });
effect(() => {
console.log(`Count is: ${state.count}`);
});
// 修改 count,effect 会自动触发
state.count++; // 输出: Count is: 1
1.2 effect 的特性
- 自动依赖收集 :
effect
会跟踪函数中访问的响应式数据(如ref
或reactive
对象的属性)。 - 自动重新运行 :当依赖的响应式数据发生变化时,
effect
内的函数会重新执行。 - 手动控制 :
effect
返回一个函数,调用它可以停止副作用的运行。
javascript
const stop = effect(() => {
console.log(`Count is: ${state.count}`);
});
// 停止 effect
stop();
state.count++; // 不会触发 console.log
1.3 使用场景
- 自定义响应式逻辑:当需要低级别的副作用控制时,
effect
是最佳选择。 - 调试或测试:直接使用
effect
可以更清晰地观察响应式系统的行为。 - 构建高级功能:如自定义 hooks 或第三方库的响应式逻辑。
二、watchEffect 简介
watchEffect
是 Vue 3 组合式 API(Composition API)中的一个高阶函数,基于 effect
构建。它简化了副作用的创建过程,适合在组件中处理响应式依赖的副作用。
2.1 watchEffect 的基本用法
watchEffect
类似于 effect
,但它直接在组件的 setup 函数或组合式 API 中使用,且会自动与组件的生命周期绑定。
javascript
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
watchEffect(() => {
console.log(`Count is: ${count.value}`);
});
return { count };
}
};
当 count
变化时,watchEffect
内的函数会自动重新运行。
2.2 watchEffect vs effect
- API 层级 :
watchEffect
是组合式 API 的一部分,专为组件设计;effect
是底层的响应式 API,适用于任何场景。 - 生命周期绑定 :
watchEffect
自动与组件的生命周期绑定(如组件销毁时自动停止);effect
需要手动管理。 - 使用场景 :
watchEffect
更适合在组件中快速创建副作用,effect
更适合底层的自定义逻辑。
2.3 watchEffect 的选项
watchEffect
支持一些选项,如 flush
和 onTrack
/onTrigger
,用于控制副作用的执行时机和调试。
javascript
watchEffect(
() => {
console.log(`Count is: ${count.value}`);
},
{
flush: 'post', // 延迟到 DOM 更新后执行
onTrack(e) {
console.log('Tracked:', e);
},
onTrigger(e) {
console.log('Triggered:', e);
}
}
);
2.3.1 flush 选项详解
flush
选项控制 watchEffect
的回调函数在响应式数据变化时的执行时机。它有三种值:pre
、post
和 sync
,每种值对应不同的行为和使用场景。
- flush: 'pre'(默认)
- 定义 :副作用在 Vue 的 DOM 更新之前运行。这意味着回调会在组件的 DOM 更新(如
v-model
或其他绑定)完成前执行。 - 行为 :确保副作用的执行不会干扰正在进行的 DOM 更新,适合需要在 DOM 更新前同步状态的场景。Vue 会将
pre
模式的副作用放入一个微任务队列,在 DOM 更新前批量执行。 - 适用场景 :
- 需要在 DOM 更新前更新状态,确保渲染使用最新值。
- 避免直接操作 DOM(因为 DOM 尚未更新)。
- 代码示例:
- 定义 :副作用在 Vue 的 DOM 更新之前运行。这意味着回调会在组件的 DOM 更新(如
javascript
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
const message = ref('');
watchEffect(
() => {
message.value = `Count is ${count.value}`;
console.log('watchEffect runs before DOM update');
},
{ flush: 'pre' }
);
return { count, message };
}
};
在上述示例中,message
会在 DOM 更新前根据 count
的变化进行更新,确保后续的渲染使用最新的 message
值。
- flush: 'post'
- 定义:副作用在 Vue 的 DOM 更新之后运行。这意味着回调会在组件的 DOM 完全更新后执行。
- 行为 :将副作用推迟到下一个微任务队列,适合需要访问更新后的 DOM 或在渲染完成后执行逻辑的场景。
post
模式确保 DOM 已反映最新状态。 - 适用场景 :
- 操作更新后的 DOM(如获取元素尺寸、触发动画)。
- 执行与渲染无关的副作用,如日志记录或异步请求。
- 代码示例:
javascript
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
watchEffect(
() => {
console.log(`Count is ${count.value}`);
// 访问 DOM
const element = document.querySelector('#myElement');
if (element) {
element.textContent = `Count: ${count.value}`;
}
},
{ flush: 'post' }
);
return { count };
}
};
在 post
模式下,watchEffect
会在 DOM 更新后运行,确保可以安全地访问最新的 DOM 状态。
- flush: 'sync'
- 定义:副作用在响应式数据变化时同步运行。每次依赖变化都会立即触发回调,不等待任何队列。
- 行为 :提供最快的响应速度,但可能导致频繁执行,增加性能开销,尤其是在批量更新时。
sync
模式不会等待微任务队列,直接在数据变化的同一事件循环中执行。 - 适用场景 :
- 需要立即响应的场景,如实时计算或同步状态。
- 调试或需要精确控制执行时机的场景。
- 代码示例:
javascript
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
const log = ref([]);
watchEffect(
() => {
log.value.push(`Count changed to ${count.value}`);
console.log('watchEffect runs synchronously');
},
{ flush: 'sync' }
);
// 批量更新
count.value++;
count.value++;
// log 将记录两次变化
return { count, log };
}
};
在 sync
模式下,每次 count
变化都会立即触发 watchEffect
,适合需要实时更新的场景,但需注意性能。
2.3.2 flush 注意事项
- 性能影响 :
sync
模式可能导致性能问题,尤其是在频繁更新时,建议谨慎使用。 - DOM 访问 :使用
post
模式以确保 DOM 已更新;pre
和sync
可能访问到未更新的 DOM。 - 批量更新 :
pre
和post
会在批量更新后运行一次,而sync
会为每次变化都运行,可能导致重复执行。
2.4 使用场景
- 动态副作用 :当需要根据多个响应式数据的变化动态执行逻辑时,
watchEffect
非常方便。 - 组件内的副作用:如根据状态变化更新 DOM、发起请求等。
- 快速原型开发:无需显式指定依赖,适合快速开发。
三、watch 简介
watch
是 Vue 3 组合式 API 中的另一个高阶函数,用于显式监听一个或多个响应式数据的变化。与 watchEffect
不同,watch
允许开发者明确指定需要监听的依赖,并提供新旧值以便进行更精确的逻辑处理。
3.1 watch 的基本用法
watch
接受三个主要参数:监听的源(可以是 ref
、reactive
对象、getter 函数或它们的数组)、回调函数以及可选的配置对象。
javascript
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
return { count };
}
};
当 count
变化时,回调函数会接收新值和旧值。
3.2 watch 的高级用法
- 监听多个源:可以监听一个数组,包含多个响应式数据。
javascript
const count = ref(0);
const name = ref('Vue');
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`Count: ${newCount}, Name: ${newName}`);
console.log(`Previous Count: ${oldCount}, Previous Name: ${oldName}`);
});
- 监听 reactive 对象的深层变化 :使用
deep
选项监听对象内部属性的变化。
javascript
import { reactive, watch } from 'vue';
const state = reactive({ user: { name: 'Vue', age: 3 } });
watch(
() => state.user,
(newUser, oldUser) => {
console.log('User changed:', newUser);
},
{ deep: true }
);
- getter 函数:通过返回响应式数据的 getter 函数,可以更精确地控制监听的范围.
javascript
watch(
() => state.user.name,
(newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`);
}
);
3.3 watch 的选项
watch
支持与 watchEffect
类似的选项,包括 flush
、deep
和 immediate
.
- immediate :设置后,
watch
会在创建时立即执行一次回调。 - deep :启用深层监听,适用于
reactive
对象或复杂数据结构。 - flush :控制回调的执行时机(
pre
、post
或sync
)。其行为与watchEffect
的flush
一致:pre
在 DOM 更新前运行,post
在 DOM 更新后运行,sync
同步运行。pre
是默认值,适合大多数场景;post
适用于需要访问更新后 DOM 的情况;sync
适合实时响应但需注意性能。
javascript
watch(
count,
(newValue, oldValue) => {
console.log(`Count changed to ${newValue}`);
},
{ immediate: true, flush: 'post' }
);
3.4 watch vs watchEffect
- 显式依赖 :
watch
需要显式指定监听的源,而watchEffect
自动收集依赖。 - 新旧值 :
watch
提供新旧值,便于比较变化;watchEffect
不提供。 - 执行时机 :
watchEffect
在创建时立即运行,且每次依赖变化都会运行;watch
只有在监听的源变化时才会触发(除非设置immediate
)。 - 使用场景 :
watch
适合需要精确控制和处理新旧值的场景;watchEffect
适合动态、广泛的副作用处理。
3.5 使用场景
- 精确监听 :当只需要监听特定数据变化时,
watch
更直观。 - 新旧值处理:需要基于新旧值进行复杂逻辑处理(如记录变化历史)。
- 异步操作:如根据数据变化发起 API 请求。
javascript
watch(count, async (newValue) => {
const response = await fetch(`/api/data/${newValue}`);
const data = await response.json();
console.log('Fetched data:', data);
});
四、effectScope 简介
effectScope
是一个用于管理一组 effect
的工具,允许开发者将多个副作用组织在一个作用域中,并统一控制它们的生命周期。
4.1 effectScope 的基本用法
创建一个 effectScope
实例,并在其中运行多个 effect
。当作用域销毁时,其中的所有 effect
都会停止。
javascript
import { reactive, effectScope, effect } from 'vue';
const scope = effectScope();
const state = reactive({ count: 0 });
scope.run(() => {
effect(() => {
console.log(`Count is: ${state.count}`);
});
effect(() => {
console.log(`Double count is: ${state.count * 2}`);
});
});
// 修改 count,两个 effect 都会触发
state.count++; // 输出: Count is: 1, Double count is: 2
// 销毁作用域,停止所有 effect
scope.stop();
state.count++; // 不会触发任何 console.log
4.2 effectScope 的特性
- 批量管理 :一个
effectScope
可以管理多个effect
,销毁作用域时,所有关联的effect
都会停止。 - 嵌套支持 :
effectScope
可以嵌套,子作用域的销毁不会影响父作用域。 - 与组件解耦 :
effectScope
不依赖于组件的生命周期,适合在非组件环境中使用。
javascript
const parentScope = effectScope();
parentScope.run(() => {
const childScope = effectScope();
childScope.run(() => {
effect(() => {
console.log('Child effect');
});
});
// 销毁子作用域
childScope.stop();
// 父作用域的 effect 仍然有效
});
4.3 使用场景
- 复杂应用管理:在大型应用中,将一组相关的副作用组织在一起,便于管理。
- 非组件环境:在非组件的模块化代码中(如工具函数或库)管理响应式副作用。
- 性能优化:通过统一销毁副作用,避免内存泄漏。
五、对比与选择
特性 | effect | watchEffect | watch | effectScope |
---|---|---|---|---|
API 层级 | 底层(@vue/reactivity) | 高层(组合式 API) | 高层(组合式 API) | 底层(@vue/reactivity) |
生命周期绑定 | 无,需手动管理 | 自动绑定组件生命周期 | 自动绑定组件生命周期 | 无,需手动管理 |
依赖管理 | 自动收集 | 自动收集 | 显式指定 | 批量管理多个 effect |
新旧值 | 无 | 无 | 提供新旧值 | 无 |
使用场景 | 自定义逻辑、测试 | 组件内动态副作用 | 精确监听、新旧值处理 | 复杂副作用管理、非组件环境 |
销毁方式 | 返回函数手动调用 | 组件销毁时自动清理 | 组件销毁时自动清理 | 调用 scope.stop() |
5.1 选择建议
- 如果你在组件中需要快速处理动态副作用,使用
watchEffect
。 - 如果需要精确监听特定数据并处理新旧值,使用
watch
。 - 如果需要低级控制或在非组件环境中工作,使用
effect
。 - 如果需要管理一组相关的副作用或避免内存泄漏,使用
effectScope
。
六、注意事项
- 避免无限循环 :在
effect
、watchEffect
或watch
中修改响应式数据可能导致无限循环。可以使用条件或显式依赖来避免。
javascript
const count = ref(0);
watchEffect(() => {
count.value++; // 错误:会导致无限循环
});
- 清理副作用 :
watchEffect
和watch
支持 cleanup 回调,用于在副作用重新运行或销毁前清理资源。
javascript
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('Timer running');
}, 1000);
onCleanup(() => {
clearInterval(timer);
});
});
-
作用域销毁 :在使用
effectScope
时,确保在适当的时机调用stop()
,否则可能导致内存泄漏。 -
调试复杂依赖 :使用
onTrack
和onTrigger
(在watchEffect
和watch
中支持)可以帮助调试复杂的响应式依赖关系。 -
flush 性能 :
sync
模式可能导致频繁执行,增加性能开销,建议优先使用pre
或post
。 -
watch 的深层监听 :启用
deep
选项可能会增加性能开销,需谨慎使用。
七、总结
effect
、watchEffect
、watch
和 effectScope
是 Vue 3 响应式系统的核心工具,各自适用于不同的场景:
effect
提供了底层的副作用控制,适合自定义逻辑和非组件环境。watchEffect
是组件友好的高阶 API,适合动态副作用处理,flush
选项(pre
、post
、sync
)提供了灵活的执行时机控制。watch
适合精确监听特定数据并处理新旧值的场景,其flush
选项与watchEffect
一致。effectScope
提供了批量管理副作用的能力,适合复杂应用和模块化代码。
通过理解这些 API 和 flush
选项的特性,开发者可以更高效地构建响应式应用,同时避免常见的陷阱。希望本文能帮助你更好地掌握 Vue 3 的响应式系统!
点个收藏,关注前端结城,一起用代码点亮前端世界!🚀