Vue3 响应式原理深度解析:Proxy 实现与依赖收集逻辑

Vue3 响应式原理深度解析:Proxy 实现与依赖收集逻辑

面向前端工程师的系统性解析:从设计目标到数据结构、从拦截细节到依赖收集与调度,再到 refcomputed 与数组、Map/Set 等容器的特殊处理。文章配套一个可运行的精简版响应式系统,帮助在源码级别建立完整心智模型。

TL;DR

  • Vue3 使用 Proxy + WeakMap 实现响应式,替代 Vue2 的 defineProperty
  • 依赖收集通过 effect 执行时的访问轨迹完成,核心是 tracktrigger
  • 数据结构:targetMap: WeakMap<object, Map<key, Set<effect>>>
  • 细粒度触发:区分 setadddelete 与数组 length、迭代依赖
  • computed 基于懒执行的 effect,用 dirty 标记与调度器缓存结果
  • ref 以对象包装原始值,通过 get/set 触发依赖
  • 性能关键:只在被访问的键上收集依赖;用 WeakMap 避免内存泄漏;按需调度

设计目标

  • 精准依赖收集:仅对访问过的属性建立依赖,减少无效更新
  • 一致语义:对象、数组、Map/Set 等统一遵循"读时收集、写时触发"
  • 良好可拓展性:支持 readonlyshallowcustom scheduler
  • 可维护性:核心概念内聚,便于调试与定位问题

核心数据结构

ts 复制代码
type EffectFn = (() => any) & { deps?: Set<Set<EffectFn>>; scheduler?: (job: () => void) => void; lazy?: boolean; };

const targetMap = new WeakMap<object, Map<any, Set<EffectFn>>>();
let activeEffect: EffectFn | null = null;
const effectStack: EffectFn[] = [];

const ITERATE_KEY = Symbol('iterate');
const MAP_KEY_ITERATE_KEY = Symbol('map_key_iterate');

effect 与依赖收集

  • effect(fn) 负责在执行过程中记录所有访问到的响应式属性
  • track(target, key) 将当前 activeEffect 放入 targetMap[target][key]Set
  • trigger(target, key, type) 找到依赖集合并逐个执行或交由调度器处理
  • 通过 effectStack 支持嵌套 effect 与正确的 activeEffect 恢复
  • 通过清理旧依赖避免"脏依赖"导致的错误触发
ts 复制代码
function cleanup(effect: EffectFn) {
  if (!effect.deps) return;
  for (const dep of effect.deps) dep.delete(effect);
  effect.deps.clear();
}

export function effect(fn: () => any, options: { scheduler?: (job: () => void) => void; lazy?: boolean } = {}): EffectFn {
  const e: EffectFn = function wrappedEffect() {
    cleanup(e);
    activeEffect = e;
    effectStack.push(e);
    try {
      return fn();
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1] || null;
    }
  } as EffectFn;
  e.deps = new Set();
  e.scheduler = options.scheduler;
  e.lazy = !!options.lazy;
  if (!e.lazy) e();
  return e;
}

function track(target: object, key: any) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps!.add(dep);
  }
}

function trigger(target: object, key: any, type: 'set' | 'add' | 'delete') {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const effects = new Set<EffectFn>();
  const addEffects = (dep?: Set<EffectFn>) => {
    if (!dep) return;
    for (const e of dep) effects.add(e);
  };
  addEffects(depsMap.get(key));
  if (type === 'add' || type === 'delete') addEffects(depsMap.get(ITERATE_KEY));
  const run = (e: EffectFn) => {
    if (e.scheduler) e.scheduler(() => e());
    else e();
  };
  for (const e of effects) run(e);
}

Proxy 拦截与 handler 设计

  • get 中进行 track,并返回属性值;对对象值递归包装以保持深度响应
  • set 区分新增与修改,从而决定是否触发迭代依赖
  • hasownKeys 读操作也需要 track,迭代依赖采用 ITERATE_KEY
  • 对数组与 Map/Set 等容器在迭代与变更时进行特殊处理
ts 复制代码
const reactiveMap = new WeakMap<object, any>();

export function reactive<T extends object>(obj: T): T {
  const existing = reactiveMap.get(obj);
  if (existing) return existing;
  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      if (typeof res === 'object' && res !== null) return reactive(res as object) as any;
      return res;
    },
    set(target, key, value, receiver) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const oldVal = (target as any)[key];
      const result = Reflect.set(target, key, value, receiver);
      if (!hadKey) trigger(target, key, 'add');
      else if (oldVal !== value) trigger(target, key, 'set');
      return result;
    },
    has(target, key) {
      const res = Reflect.has(target, key);
      track(target, key);
      return res;
    },
    ownKeys(target) {
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) trigger(target, key, 'delete');
      return result;
    }
  });
  reactiveMap.set(obj, proxy);
  return proxy as T;
}

ref 与 computed 的实现

  • ref 将原始值包成对象,在 valueget/settrack/trigger
  • computed 用懒执行的 effect,首次访问求值,后续由依赖变更时标记为 dirty
ts 复制代码
export function ref<T>(raw: T) {
  const r = {
    get value() {
      track(r, 'value');
      return raw;
    },
    set value(v: T) {
      raw = v;
      trigger(r, 'value', 'set');
    }
  };
  return r;
}

export function computed<T>(getter: () => T) {
  let cached: T;
  let dirty = true;
  const runner = effect(() => {
    cached = getter();
  }, { lazy: true, scheduler: job => job() });
  return {
    get value() {
      if (dirty) {
        runner();
        dirty = false;
      }
      track(this, 'value');
      return cached!;
    }
  };
}

可运行的最小完整示例

ts 复制代码
// 基本使用
const state = reactive({ count: 0, nested: { a: 1 } });
const doubled = computed(() => state.count * 2);

effect(() => {
  document.querySelector('#app')!.textContent = `count=${state.count}, doubled=${doubled.value}`;
});

setInterval(() => {
  state.count++;
}, 1000);

迭代依赖与数组/容器细节

  • 数组:length 变化会影响索引依赖;迭代依赖通过 ITERATE_KEY
  • Map/Set:键迭代与值迭代分别跟踪,可用 MAP_KEY_ITERATE_KEYITERATE_KEY
  • 容器方法需要"仪器化",例如 set.addmap.set 在内部调用 trigger
  • 迭代读取(如 for...infor...ofObject.keysownKeys)都需要 track(ITERATE_KEY)

调度器与批处理

  • effect 可选 scheduler,用于将同步触发改为异步或批处理
  • 常见实现是微任务队列,将多次触发合并后统一执行
ts 复制代码
const queue = new Set<Function>();
let flushing = false;
function queueJob(job: Function) {
  queue.add(job);
  if (!flushing) {
    flushing = true;
    Promise.resolve().then(() => {
      for (const j of queue) j();
      queue.clear();
      flushing = false;
    });
  }
}

const state2 = reactive({ x: 0 });
const e2 = effect(() => {
  console.log(state2.x);
}, { scheduler: queueJob });
state2.x = 1;
state2.x = 2;

性能与内存管理

  • WeakMap 避免持有对已释放对象的强引用,降低泄漏风险
  • 精准依赖收集减少无效更新;避免在未访问的属性上建立依赖
  • 清理旧依赖确保触发集合不膨胀;对高频更新用调度器批处理

常见坑与排查

  • 未在 effect 中访问响应式数据,导致未收集依赖
  • 忽略迭代依赖,增删属性或容器项不触发更新
  • 忽略数组 length 与索引的耦合,更新行为不一致
  • 循环触发与递归更新,需借助调度器或状态分离
  • 深浅响应不当:shallowReactive 只包装一层;readonly 禁止写入

与 Vue3 源码的差异与拓展

  • 示例为教学版,省略了 readonlyshallowtoRawmarkRaw
  • 未覆盖 Map/Set 的完整仪器化、TrackOpTypes/TriggerOpTypes 的枚举细分
  • 源码对数组、TypedArray、内建集合均做了更细粒度优化与边界处理

总结

Vue3 响应式的核心在于"读时收集、写时触发"这条主线。以 Proxy 为载体、以 WeakMap → Map → Set 为依赖索引,辅以 effect 栈与调度器策略,既保证正确性与性能,又为容器类型与高级特性预留扩展空间。理解这些细节后,能够在业务中更精准地使用 refreactivecomputed,在复杂场景中定位与优化更新行为。

相关推荐
HIT_Weston1 小时前
59、【Ubuntu】【Gitlab】拉出内网 Web 服务:Gitlab 配置审视(三)
前端·ubuntu·gitlab
syt_10131 小时前
grid布局之-子项放置2
前端·javascript·css
韩曙亮1 小时前
【Web APIs】JavaScript 动画 ② ( 缓动动画 | 步长计算取整 )
前端·javascript·动画·web apis·缓动动画·匀速动画
by__csdn1 小时前
javascript 性能优化实战:异步和延迟加载
开发语言·前端·javascript·vue.js·性能优化·typescript·ecmascript
by__csdn1 小时前
JavaScript性能优化实战:减少DOM操作全方位攻略
前端·javascript·vue.js·react.js·性能优化·typescript
xiaoxue..1 小时前
从 “手动搬砖“ 到 “自动施法“:界面开发的三次 “渡劫“ 升级记
前端·前端框架·vue
Monly211 小时前
Vue:使用v-if v-else加载两个el-table 在切换时,会出现数据在家混乱 数据加载不全的情况
前端·javascript·vue.js
南知意-1 小时前
一个基于 Vue、Datav、Echart 框架开源免费的数据大屏可视化系统
前端·javascript·vue.js·开源软件·大屏项目
修己xj1 小时前
FlatNas:打造你的专属浏览器仪表盘,一个集优雅与实用于一身的开源导航页
前端