Skip to content
字数
2487 字
阅读时间
11 分钟

在 Vue 3 中,effect 是响应式系统的核心调度机制,替代了 Vue 2 中的 Watcher。它的设计解决了 Vue 2 中 Watcher 的复杂性和局限性,同时让响应式逻辑更灵活、更贴近 JavaScript 原生语义。

一、effect 的核心作用与设计理念

effect 本质是一个“副作用函数”管理器:当一个函数(如组件渲染函数、计算属性、watch 回调)依赖响应式数据时,effect 会将其包裹,并在数据变化时自动重新执行该函数(触发“副作用”)。

核心理念:

  • 明确“依赖收集”与“触发更新”的边界effect 函数执行时,会自动追踪其内部访问的响应式数据(依赖收集);当这些数据变化时,effect 会重新执行(触发更新)。
  • 轻量化与灵活性:相比 Vue 2 中功能繁杂的 Watcher(同时处理组件渲染、watchcomputed 等),effect 更精简,通过不同的配置实现多样化需求。

二、effect 的实现机制

effect 的实现涉及依赖收集依赖存储触发更新三个核心环节,下面结合源码逻辑(简化版)解析:

1. 基本结构:创建 effect 函数

effect 函数的作用是包裹“副作用函数”,并返回一个可手动控制的响应式函数。

javascript
// 全局变量:当前激活的 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 函数访问时(触发 Proxyget 拦截),需要通过 track 函数记录“数据-属性-effect”的映射关系,即“谁依赖了这个数据的这个属性”。

javascript
// 依赖映射表: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);
  }
}
  • 数据结构:使用 WeakMaptargetMap)+ MapdepsMap)+ Setdep)的嵌套结构,高效存储依赖关系,且不会阻止 target 被垃圾回收。
  • 双向记录dep 中记录 effecteffect.deps 中记录 dep,便于后续更新时快速找到所有依赖,或清理无效依赖。

3. 触发更新:trigger 函数

当响应式数据被修改时(触发 Proxyset 拦截),通过 trigger 函数找到该数据属性对应的所有 effect,并执行它们(触发副作用)。

javascript
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. 完整流程示例

javascript
// 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 触发 Proxyget 拦截,调用 track(obj, 'count'),将 effectFn 加入 obj.count 的依赖集合。
  • 修改 obj.count 触发 Proxyset 拦截,调用 trigger(obj, 'count'),找到依赖的 effectFn 并执行,输出新值。

三、为什么用 effect 替代 Vue 2 的 Watcher

Vue 2 的 Watcher 是一个“万能容器”,同时承担了组件渲染、watchcomputed 等多种角色,导致其设计复杂且耦合度高。effect 的设计则解决了这些问题:

1. 简化概念,降低耦合

  • Vue 2 中,Watcher 分为渲染 Watcher(组件渲染)、用户 Watcherwatch 选项)、计算属性 Watchercomputed),不同类型的 Watcher 逻辑混杂在同一类中,难以维护。

  • Vue 3 中,effect 是一个通用的副作用容器,通过配置项(如 lazyscheduler)实现不同功能:

    • 组件渲染: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
    const double = ref(0);
    const count = ref(1);
    
    effect(() => {
      double.value = count.value * 2; // 当 count 变化时,自动更新 double
    });
    这种设计让响应式逻辑更贴近原生 JavaScript,降低了使用门槛。

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 是对响应式副作用管理的彻底重构,其核心机制是:

  1. 通过 effect 包裹副作用函数,执行时标记 activeEffect
  2. 响应式数据被访问时,track 函数记录“数据-属性-effect”的依赖关系;
  3. 数据变化时,trigger 函数找到对应的 effect 并执行(支持通过 scheduler 自定义执行逻辑)。

相比 Vue 2 的 Watchereffect 具有概念简洁依赖精准灵活性高扩展性强等优势,是 Vue 3 响应式系统性能提升和开发体验优化的核心基础。

贡献者

The avatar of contributor named as chenjie chenjie

页面历史

撰写