vue3源码解析:Effect的实现

153 阅读9分钟

上文,我们详细分析了 Vue 的依赖收集,包括其核心数据结构(Link、Dep)和依赖收集的完整流程。而依赖收集中频繁提到的 ReactiveEffect,作为订阅(Subscriber)的具体实现,是整个响应式系统中另一个重要的组成部分。它不仅负责执行副作用函数,还要处理依赖的收集、清理以及更新的调度。本文将深入分析 Effect 系统的实现。

一、ReactiveEffect 类的设计与实现

1. 核心数据结构

// 源码位置: packages/reactivity/src/effect.ts
export interface ReactiveEffectRunner<T = any> {
  (): T;
  effect: ReactiveEffect;
}

export class ReactiveEffect<T = any> implements Subscriber {
  // 依赖链表头部
  deps?: Link = undefined;
  // 依赖链表尾部
  depsTail?: Link = undefined;
  // 效果状态标志位
  flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING;
  // 链表中的下一个订阅者
  next?: Subscriber = undefined;
  // 清理函数
  cleanup?: () => void = undefined;
  // 调度器
  scheduler?: EffectScheduler = undefined;
  // 停止时的回调
  onStop?: () => void;
  // 调试相关的回调
  onTrack?: (event: DebuggerEvent) => void;
  onTrigger?: (event: DebuggerEvent) => void;

  constructor(public fn: () => T) {
    // 如果存在活跃的作用域,将当前效果添加到作用域中
    if (activeEffectScope && activeEffectScope.active) {
      activeEffectScope.effects.push(this);
    }
  }

  // 暂停当前 effect
  pause(): void;

  // 恢复当前 effect
  resume(): void;

  // 通知更新
  notify(): void;

  // 执行副作用函数
  run(): T;

  // 停止 effect
  stop(): void;

  // 触发更新
  trigger(): void;

  // 如果是脏的则运行
  runIfDirty(): void;

  // 获取脏状态
  get dirty(): boolean;
}

2. 状态标志位系统

export enum EffectFlags {
  ACTIVE = 1 << 0, // 效果是否处于活跃状态
  RUNNING = 1 << 1, // 效果是否正在运行
  TRACKING = 1 << 2, // 是否正在追踪依赖
  NOTIFIED = 1 << 3, // 是否已被通知更新
  DIRTY = 1 << 4, // 是否需要重新计算
  ALLOW_RECURSE = 1 << 5, // 是否允许递归触发
  PAUSED = 1 << 6, // 是否被暂停
}

3. 核心方法实现

run 方法 - 执行副作用函数

run(): T {
  // 如果效果已停止,直接执行函数不收集依赖
  if (!(this.flags & EffectFlags.ACTIVE)) {
    return this.fn()
  }

  // 设置运行标记
  this.flags |= EffectFlags.RUNNING
  // 清理旧的依赖
  cleanupEffect(this)
  // 准备新的依赖收集
  prepareDeps(this)

  const prevEffect = activeSub
  const prevShouldTrack = shouldTrack
  activeSub = this
  shouldTrack = true

  try {
    return this.fn()
  } finally {
    cleanupDeps(this)
    activeSub = prevEffect
    shouldTrack = prevShouldTrack
    this.flags &= ~EffectFlags.RUNNING
  }
}

run 方法是 effect 系统的核心执行方法,负责:

  1. 状态检查

    • 检查 effect 是否处于活跃状态
    • 非活跃状态直接执行函数但不收集依赖
  2. 执行环境准备

    • 设置 RUNNING 标志
    • 清理旧的依赖关系
    • 准备新的依赖收集环境
  3. 上下文管理

    • 保存当前的全局 effect 状态
    • 设置新的追踪状态
    • 确保依赖收集的正确性
  4. 安全执行

    • 使用 try-finally 确保状态恢复
    • 执行完成后清理依赖
    • 恢复之前的全局状态

trigger 方法 - 触发更新

trigger(): void {
  if (this.flags & EffectFlags.PAUSED) {
    // 如果被暂停,加入暂停队列
    pausedQueueEffects.add(this)
  } else if (this.scheduler) {
    // 有调度器则使用调度器执行
    this.scheduler()
  } else {
    // 否则直接检查并运行
    this.runIfDirty()
  }
}

trigger 方法是响应式更新的触发入口,主要职责包括:

  1. 状态判断

    • 检查 effect 是否被暂停
    • 判断是否存在自定义调度器
  2. 更新策略

    • 暂停状态:将 effect 加入暂停队列
    • 有调度器:使用自定义调度器处理
    • 默认情况:直接执行更新
  3. 性能优化

    • 支持更新暂停机制
    • 通过调度器实现更新控制
    • 脏检查避免不必要的更新

stop 方法 - 停止效果

stop(): void {
  if (this.flags & EffectFlags.ACTIVE) {
    // 移除所有依赖
    for (let link = this.deps; link; link = link.nextDep) {
      removeSub(link)
    }
    this.deps = this.depsTail = undefined
    // 执行清理
    cleanupEffect(this)
    this.onStop && this.onStop()
    // 清除活跃标记
    this.flags &= ~EffectFlags.ACTIVE
  }
}

stop 方法负责 effect 的停止和清理工作,包括:

  1. 状态检查

    • 确认 effect 当前是否处于活跃状态
    • 避免重复停止操作
  2. 依赖清理

    • 遍历并移除所有依赖关系
    • 重置依赖链表的头尾指针
    • 执行清理函数
  3. 生命周期处理

    • 调用 onStop 回调
    • 清除活跃状态标记
    • 完成 effect 的生命周期
  4. 内存管理

    • 确保所有引用被正确清除
    • 防止内存泄漏
    • 支持垃圾回收

notify 方法 - 通知更新

notify(): void {
  // 如果当前正在运行且不允许递归,则直接返回
  if (
    this.flags & EffectFlags.RUNNING &&
    !(this.flags & EffectFlags.ALLOW_RECURSE)
  ) {
    return
  }
  // 如果未被通知过,则加入批处理队列
  if (!(this.flags & EffectFlags.NOTIFIED)) {
    batch(this)
  }
}

notify 方法是响应式系统中的关键方法,它负责:

  1. 递归控制

    • 检查当前 effect 是否正在运行
    • 通过 ALLOW_RECURSE 标志控制是否允许递归更新
  2. 批量处理

    • 使用 NOTIFIED 标志避免重复通知
    • 通过 batch 函数将更新加入批处理队列
  3. 更新触发

    • 最终通过批处理系统统一处理所有更新
    • 确保更新的有序性和性能

pause 方法 - 暂停 effect

pause(): void {
  this.flags |= EffectFlags.PAUSED
}

pause 方法实现了 effect 的暂停功能,主要职责包括:

  1. 状态管理

    • 设置 PAUSED 标志位
    • 不影响其他状态标志
    • 保持依赖关系不变
  2. 更新控制

    • 暂停后的更新会被加入暂停队列
    • 不会立即触发更新
    • 等待后续恢复
  3. 性能优化

    • 支持临时禁用某些更新
    • 可用于批量更新场景
    • 减少不必要的计算

resume 方法 - 恢复 effect

resume(): void {
  if (this.flags & EffectFlags.PAUSED) {
    this.flags &= ~EffectFlags.PAUSED
    if (pausedQueueEffects.has(this)) {
      pausedQueueEffects.delete(this)
      this.trigger()
    }
  }
}

resume 方法负责恢复暂停的 effect,其功能包括:

  1. 状态恢复

    • 检查是否处于暂停状态
    • 清除 PAUSED 标志位
    • 保持其他状态不变
  2. 更新处理

    • 检查暂停队列中是否有待处理的更新
    • 从暂停队列中移除
    • 触发待处理的更新
  3. 执行优化

    • 只处理确实需要更新的 effect
    • 避免重复触发
    • 确保更新的连续性
  4. 生命周期管理

    • 与 pause 配对使用
    • 支持暂停/恢复的完整周期
    • 维护 effect 的正确状态

二、Watch 中的 Effect 应用

1. Watch 中的 Effect 创建和执行

// 源码位置: packages/reactivity/src/watch.ts
export function watch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb?: WatchCallback | null,
  options: WatchOptions = EMPTY_OBJ
): WatchHandle {
  // 1. 创建 getter 函数
  let getter: () => any;
  if (isRef(source)) {
    getter = () => source.value;
  } else if (isReactive(source)) {
    getter = () => traverse(source);
  } else if (isFunction(source)) {
    getter = source;
  }

  // 2. 创建 effect 实例
  const effect = new ReactiveEffect(getter);

  // 3. 配置调度器
  effect.scheduler = scheduler
    ? () => scheduler(job, false)
    : (job as EffectScheduler);

  // 4. 设置清理函数
  const cleanup = (effect.onStop = () => {
    const cleanups = cleanupMap.get(effect);
    if (cleanups) {
      if (call) {
        call(cleanups, WatchErrorCodes.WATCH_CLEANUP);
      } else {
        for (const cleanup of cleanups) cleanup();
      }
      cleanupMap.delete(effect);
    }
  });

  // 5. 配置开发环境调试钩子
  if (__DEV__) {
    effect.onTrack = options.onTrack;
    effect.onTrigger = options.onTrigger;
  }

  // 6. 首次执行
  if (cb) {
    if (immediate) {
      job(true);
    } else {
      oldValue = effect.run();
    }
  } else if (scheduler) {
    scheduler(job.bind(null, true), true);
  } else {
    effect.run();
  }

  // 7. 暴露控制方法
  const watchHandle = {
    effect,
    pause: effect.pause.bind(effect),
    resume: effect.resume.bind(effect),
    stop: () => {
      effect.stop();
      cleanup();
    },
  };

  return watchHandle;
}

// job 函数定义
const job = (immediateFirstRun?: boolean) => {
  if (!(effect.flags & EffectFlags.ACTIVE)) {
    return;
  }

  if (cb) {
    // 获取新值
    const newValue = effect.run();

    // 判断是否需要触发回调
    if (deep || immediateFirstRun || hasChanged(newValue, oldValue)) {
      // 执行清理
      if (cleanup) {
        cleanup();
      }

      // 调用回调
      cb(newValue, oldValue, onCleanup);

      // 更新旧值
      oldValue = newValue;
    }
  } else {
    effect.run();
  }
};

每个步骤的详细说明:

  1. 创建 getter 函数
// 源码位置: packages/reactivity/src/watch.ts
let getter: () => any;
if (isRef(source)) {
  getter = () => source.value;
} else if (isReactive(source)) {
  getter = () => traverse(source);
} else if (isFunction(source)) {
  getter = source;
}
  • 根据 source 类型创建不同的 getter:

    • ref:返回 .value
    • reactive:使用 traverse 递归访问
    • function:直接使用函数本身
  • getter 函数决定了依赖收集的范围

  • getter 函数就是用来获取你 watch 的那个值

  1. 创建 effect 实例
// 源码位置: packages/reactivity/src/watch.ts
const effect = new ReactiveEffect(getter)

// ReactiveEffect 构造函数
// 源码位置: packages/reactivity/src/effect.ts
constructor(public fn: () => T) {
  if (activeEffectScope && activeEffectScope.active) {
    activeEffectScope.effects.push(this)
  }
}
  • 使用 getter 创建 ReactiveEffect 实例
  • 此时会自动关联到当前的 effectScope
  • 为后续的依赖收集做准备
  1. 配置调度器
// 源码位置: packages/reactivity/src/watch.ts
effect.scheduler = scheduler
  ? () => scheduler(job, false)
  : (job as EffectScheduler);

// job 函数定义
const job = (immediateFirstRun?: boolean) => {
  if (!(effect.flags & EffectFlags.ACTIVE)) {
    return;
  }
  if (cb) {
    const newValue = effect.run();
    if (deep || immediateFirstRun || hasChanged(newValue, oldValue)) {
      cb(newValue, oldValue, onCleanup);
      oldValue = newValue;
    }
  } else {
    effect.run();
  }
};
  • 设置 effect.scheduler
  • 可以是自定义调度器或默认的 job 函数
  • 调度器负责控制更新时机和方式
  1. 设置清理函数
// 源码位置: packages/reactivity/src/watch.ts
const cleanup = (effect.onStop = () => {
  const cleanups = cleanupMap.get(effect);
  if (cleanups) {
    if (call) {
      call(cleanups, WatchErrorCodes.WATCH_CLEANUP);
    } else {
      for (const cleanup of cleanups) cleanup();
    }
    cleanupMap.delete(effect);
  }
});

// 注册清理函数
const onCleanup = (fn: () => void) => {
  let cleanups = cleanupMap.get(effect);
  if (!cleanups) {
    cleanupMap.set(effect, (cleanups = []));
  }
  cleanups.push(fn);
};
  • 配置 effect.onStop 处理停止时的清理
  • 管理 cleanupMap 中的清理函数
  • 确保资源正确释放
  1. 配置开发环境调试钩子
// 源码位置: packages/reactivity/src/watch.ts
if (__DEV__) {
  effect.onTrack = options.onTrack;
  effect.onTrigger = options.onTrigger;
}

// 调试事件接口定义
// 源码位置: packages/reactivity/src/effect.ts
export interface DebuggerEvent {
  effect: ReactiveEffect;
  target: object;
  type: TrackOpTypes | TriggerOpTypes;
  key: any;
}
  • 设置 onTrack 用于跟踪依赖收集
  • 设置 onTrigger 用于跟踪依赖触发
  • 仅在开发环境中生效
  1. 首次执行
// 源码位置: packages/reactivity/src/watch.ts
if (cb) {
  if (immediate) {
    job(true);
  } else {
    oldValue = effect.run();
  }
} else if (scheduler) {
  scheduler(job.bind(null, true), true);
} else {
  effect.run();
}
  • 根据配置选择执行策略:

    • immediate + callback:直接执行 job
    • 有 callback:运行 effect 获取旧值
    • 有 scheduler:通过调度器执行
    • 默认:直接运行 effect
  1. 暴露控制方法
// 源码位置: packages/reactivity/src/watch.ts
const watchHandle = {
  effect,
  pause: effect.pause.bind(effect),
  resume: effect.resume.bind(effect),
  stop: () => {
    effect.stop();
    cleanup();
  },
};

return watchHandle;
  • 暴露 effect 实例
  • 绑定 pause/resume 方法
  • 提供 stop 方法用于完全停止

2. Watch 中的 Effect 生命周期

通过分析 effect 在 watch 中的创建和执行,我们可以得出 effect 在 watch 中的生命周期分为以下几个阶段:

  1. 创建阶段

    • 创建 ReactiveEffect 实例
    • 配置调度器和清理函数
    • 设置调试钩子(开发环境)
  2. 初始化阶段

    • 根据配置执行首次运行
    • 收集初始依赖
    • 获取初始值(如果需要)
  3. 更新阶段

    • 依赖变化触发 scheduler
    • scheduler 执行 job 函数
    • job 函数对比新旧值并触发回调
  4. 清理阶段

    • 执行注册的清理函数
    • 清除依赖关系
    • 释放相关资源
  5. 控制阶段

    • 支持通过 pause/resume 控制监听状态
    • 可以通过 stop 完全停止监听
    • 保持对监听器的完整控制能力

三、实际应用示例

1. 基础的响应式更新

const count = ref(0);
effect(() => {
  console.log(count.value);
});
count.value++; // 触发更新

执行流程:

  1. 创建 effect 实例
  2. 首次运行收集依赖
  3. 修改值触发 trigger
  4. 通过调度系统执行更新

2. Watch 的实现

watch(
  () => count.value,
  (newVal, oldVal) => {
    console.log(newVal, oldVal);
  }
);

执行流程:

  1. 创建 getter 函数
  2. 创建 effect 实例
  3. 设置调度器和回调
  4. 首次运行收集依赖
  5. 后续更新时触发回调

四、总结

本文析了 Vue 的 Effect 系统,包括其核心数据结构(ReactiveEffect)和关键方法的实现。通过分析 watch 函数内 effect 的执行部分,我们了解了 effect 的详细执行流程和实际作用。下文来详细分析调度系统的具体实现。