在 Vue 3 中,effect 是响应式系统的核心调度机制,替代了 Vue 2 中的 Watcher。它的设计解决了 Vue 2 中 Watcher 的复杂性和局限性,同时让响应式逻辑更灵活、更贴近 JavaScript 原生语义。
一、effect 的核心作用与设计理念
effect 本质是一个“副作用函数”管理器:当一个函数(如组件渲染函数、计算属性、watch 回调)依赖响应式数据时,effect 会将其包裹,并在数据变化时自动重新执行该函数(触发“副作用”)。
核心理念:
- 明确“依赖收集”与“触发更新”的边界:
effect函数执行时,会自动追踪其内部访问的响应式数据(依赖收集);当这些数据变化时,effect会重新执行(触发更新)。 - 轻量化与灵活性:相比 Vue 2 中功能繁杂的
Watcher(同时处理组件渲染、watch、computed等),effect更精简,通过不同的配置实现多样化需求。
二、effect 的实现机制
effect 的实现涉及依赖收集、依赖存储、触发更新三个核心环节,下面结合源码逻辑(简化版)解析:
1. 基本结构:创建 effect 函数
effect 函数的作用是包裹“副作用函数”,并返回一个可手动控制的响应式函数。
// 全局变量:当前激活的 effect(用于依赖收集时标记谁在访问数据)
let activeEffect = null;
// 用于创建副作用函数的工厂函数
function effect(fn, options = {}) {
// 包装原始函数,增强其能力(如错误处理、调度控制)
const effectFn = () => {
try {
// 执行副作用函数前,将当前 effect 标记为激活状态
activeEffect = effectFn;
// 执行原始函数(此时访问响应式数据会触发 get 拦截,进行依赖收集)
return fn();
} finally {
// 执行完毕后,重置激活状态(避免污染其他 effect)
activeEffect = null;
}
};
// 存储依赖的集合(每个 effect 对应一组依赖它的响应式数据)
effectFn.deps = [];
// 保存配置项(如 lazy、scheduler 等)
effectFn.options = options;
// 非懒执行模式下,立即执行一次副作用函数(触发首次依赖收集)
if (!options.lazy) {
effectFn();
}
// 返回包装后的 effect 函数(可手动调用)
return effectFn;
}- 关键设计:
activeEffect全局变量用于标记“当前正在执行的副作用函数”,使得响应式数据被访问时(get拦截)能知道该将谁加入依赖列表。
2. 依赖收集:track 函数
当响应式数据被 effect 函数访问时(触发 Proxy 的 get 拦截),需要通过 track 函数记录“数据-属性-effect”的映射关系,即“谁依赖了这个数据的这个属性”。
// 依赖映射表:target -> key -> Set<effect>
// 含义:某个对象(target)的某个属性(key)被哪些 effect 依赖
const targetMap = new WeakMap();
function track(target, key) {
// 若当前没有激活的 effect(如非 effect 函数中访问数据),无需收集
if (!activeEffect) return;
// 1. 从 targetMap 中获取 target 对应的依赖表(若不存在则创建)
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 2. 从 depsMap 中获取 key 对应的 effect 集合(若不存在则创建)
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 3. 将当前激活的 effect 加入依赖集合(去重,避免重复收集)
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
// 同时在 effect 的 deps 中记录依赖(用于后续清理)
activeEffect.deps.push(dep);
}
}- 数据结构:使用
WeakMap(targetMap)+Map(depsMap)+Set(dep)的嵌套结构,高效存储依赖关系,且不会阻止target被垃圾回收。 - 双向记录:
dep中记录effect,effect.deps中记录dep,便于后续更新时快速找到所有依赖,或清理无效依赖。
3. 触发更新:trigger 函数
当响应式数据被修改时(触发 Proxy 的 set 拦截),通过 trigger 函数找到该数据属性对应的所有 effect,并执行它们(触发副作用)。
function trigger(target, key) {
// 1. 从 targetMap 中获取 target 对应的依赖表
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 2. 从 depsMap 中获取 key 对应的 effect 集合
const dep = depsMap.get(key);
if (!dep) return;
// 3. 复制一份 effect 集合(避免执行时修改原集合导致遍历异常)
const effects = new Set(dep);
// 4. 执行所有依赖的 effect 函数
effects.forEach(effectFn => {
// 若 effect 配置了 scheduler(调度器),则优先执行 scheduler
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
// 否则直接执行 effect 函数
effectFn();
}
});
}- 调度器(scheduler):
effect的核心扩展点。通过options.scheduler可以自定义effect的执行时机(如防抖、节流)、执行方式(如放入微任务队列异步执行),Vue 3 的组件更新、watch等功能均依赖此机制。
4. 完整流程示例
// 1. 创建响应式对象
const obj = reactive({ count: 0 });
// 2. 创建 effect 副作用函数(依赖 obj.count)
effect(() => {
console.log('count 变化了:', obj.count);
});
// 首次执行输出:count 变化了:0
// 3. 修改响应式数据(触发 set 拦截 -> trigger -> effect 重新执行)
obj.count = 1; // 输出:count 变化了:1
obj.count = 2; // 输出:count 变化了:2流程解析:
- 执行
effect时,activeEffect被设为当前effectFn,随后执行console.log(...)并访问obj.count。 - 访问
obj.count触发Proxy的get拦截,调用track(obj, 'count'),将effectFn加入obj.count的依赖集合。 - 修改
obj.count触发Proxy的set拦截,调用trigger(obj, 'count'),找到依赖的effectFn并执行,输出新值。
三、为什么用 effect 替代 Vue 2 的 Watcher?
Vue 2 的 Watcher 是一个“万能容器”,同时承担了组件渲染、watch、computed 等多种角色,导致其设计复杂且耦合度高。effect 的设计则解决了这些问题:
1. 简化概念,降低耦合
Vue 2 中,
Watcher分为渲染 Watcher(组件渲染)、用户 Watcher(watch选项)、计算属性 Watcher(computed),不同类型的Watcher逻辑混杂在同一类中,难以维护。Vue 3 中,
effect是一个通用的副作用容器,通过配置项(如lazy、scheduler)实现不同功能:- 组件渲染:
effect配合scheduler实现异步更新队列; computed:通过lazy: true实现懒计算 + 缓存;watch:通过监听数据变化后执行指定effect实现。
这种“基础核心 + 配置扩展”的设计更灵活,耦合度更低。
- 组件渲染:
2. 更精准的依赖收集
- Vue 2 中,
Watcher依赖收集的粒度是“整个组件”,即使组件中只有一个属性变化,也会触发整个组件的重新渲染(后续通过shouldComponentUpdate优化)。 - Vue 3 中,
effect会精确追踪函数内部访问的响应式数据,只有当被访问的属性变化时,effect才会重新执行。例如:javascript这种“按实际访问追踪”的机制,避免了不必要的更新,性能更优。// Vue 3 中,只有 count 变化时才会执行,msg 变化不影响 effect(() => { if (obj.flag) { console.log(obj.count); } });
3. 原生语义与灵活性
- Vue 2 的
Watcher是框架内部的黑盒概念,开发者无法直接操作。 - Vue 3 的
effect是一个暴露给开发者的 API,可直接用于创建自定义响应式逻辑,例如:javascript这种设计让响应式逻辑更贴近原生 JavaScript,降低了使用门槛。const double = ref(0); const count = ref(1); effect(() => { double.value = count.value * 2; // 当 count 变化时,自动更新 double });
4. 支持调度器(scheduler),优化更新时机
Vue 2 中,
Watcher的更新时机由框架内部固定(同步或通过nextTick异步),开发者难以自定义。Vue 3 的
effect通过scheduler支持自定义更新逻辑,例如:- 实现防抖:
scheduler: debounce(effectFn, 100); - 放入微任务队列异步执行(组件更新默认行为);
- 条件执行:仅在特定条件下才执行
effect。
这为性能优化和复杂场景提供了更大的灵活性(如 Vue 3 的“批处理更新”依赖此机制)。
- 实现防抖:
5. 更好的 TypeScript 支持与tree-shaking
- Vue 2 的
Watcher基于 ES5 类设计,类型定义复杂,且难以按需引入。 - Vue 3 的
effect是函数式设计,类型清晰,且可单独引入(import { effect } from 'vue'),配合 tree-shaking 减少打包体积。
四、总结
Vue 3 的 effect 是对响应式副作用管理的彻底重构,其核心机制是:
- 通过
effect包裹副作用函数,执行时标记activeEffect; - 响应式数据被访问时,
track函数记录“数据-属性-effect”的依赖关系; - 数据变化时,
trigger函数找到对应的effect并执行(支持通过scheduler自定义执行逻辑)。
相比 Vue 2 的 Watcher,effect 具有概念简洁、依赖精准、灵活性高、扩展性强等优势,是 Vue 3 响应式系统性能提升和开发体验优化的核心基础。