Proxy / Reflect 与响应式原理:完整手写一个 mini Vue3 响应式系统

问题场景

Vue3 的响应式、状态管理库(zustand/valtio)、数据劫持、字段脱敏------现代前端做"数据一变,界面/逻辑自动跟着变",底层全靠 Proxy + Reflect

很多人在用 Vue3 时只知道"ref/reactive 会让数据响应式",但遇到这几个问题就懵:

  1. 为什么 reactive 里的对象要深层遍历?新增属性为什么自动响应?
  2. 为什么数组下标/长度修改在 Vue3 能响应、Vue2 却不行?
  3. computedwatch 底层是怎么"感知"到依赖变化的?
  4. Proxy 里的 get 为什么不直接 return obj[key],非要包一层 Reflect.get

理解这些,你不仅能用好 Vue3,还能手写 mini 响应式,一眼看穿 zustand 等库的底层。本文从零手写一个完整可用的 mini 响应式系统。

原理深入

1. Proxy:给对象加"读写的钩子"

Proxynew Proxy(target, handler) 包一层,handler 里的 get/set/has/deleteProperty 等方法可以拦截对 target 的所有操作:

js 复制代码
const target = { name: 'x', count: 0 };
const p = new Proxy(target, {
  get(obj, key) { console.log(`读取 ${key}`); return obj[key]; },
  set(obj, key, val) { obj[key] = val; console.log(`写入 ${key}=${val}`); return true; },
});
p.count;      // 读取 count
p.count = 5;  // 写入 count=5

为什么拦截 set 必须 return true? 在严格模式下,set 返回 false 会当做出错(抛 TypeError)。所以要么 return true,要么用 Reflect.set 返回的真实结果。

2. Reflect:为什么要配合 Proxy 用

Reflect 提供和 Proxy 拦截器一一对应 的方法。在 Proxy 里用它而不是直接操作 target,有两个关键原因

原因 A:receiver 保证 getter 里的 this 正确

js 复制代码
const target = {
  name: 'x',
  get full() { return `${this.name}!`; },  // 这里的 this 指向谁?
};

// 直接 obj[key]:this 指向 target 本身,OK
// 但如果是继承/代理场景,this 可能错
const p = new Proxy(target, {
  get(obj, key, receiver) {
    return obj[key];          // ❌ 若 target 有 getter,this 指向 obj(target),可能不对
    return Reflect.get(obj, key, receiver);  // ✅ receiver 保证 getter 的 this 正确
  },
});

原因 B:Reflect.set 返回布尔值(成功与否,符合严格模式要求)

3. Proxy vs Object.defineProperty(Vue3 为何换)

能力 defineProperty Proxy
监听新增/删除属性 ❌ 需重新 define ✅ 天然拦截
监听数组下标/索引 ❌ 需特殊 hack ✅ 天然拦截
监听数组 length ❌ 需 hack ✅ 拦截
深层对象 需一次性递归 需懒递归(首次访问才深层代理)
拦截能力 仅 get/set get/set/has/delete/ownKeys... 全量

Vue2 的两个顽疾------新增属性、数组下标 不能响应(this.obj.newProp = 1 无效)------在 Vue3 用 Proxy 彻底解决。

实战代码:完整手写 mini reactive(Vue3 核心)

一个标准响应式系统 = Proxy 拦截(get 收集依赖 / set 触发更新)+ 依赖收集表 + effect 调度。分四步实现。

Step 1:依赖收集表(WeakMap<target, Map<key, Set>>)

js 复制代码
const depsMap = new WeakMap();   // 用 WeakMap:target 被 GC 时自动回收
let activeEffect = null;         // 当前正在运行的 effect

function track(target, key) {
  if (!activeEffect) return;     // 没有活跃 effect(如普通读取)就跳过
  let deps = depsMap.get(target);
  if (!deps) depsMap.set(target, (deps = new Map()));
  if (!deps.has(key)) deps.set(key, new Set());
  deps.get(key).add(activeEffect);   // 读取时:把当前 effect 记到该 key 的依赖集合
}

function trigger(target, key) {
  const deps = depsMap.get(target);
  if (!deps) return;
  deps.get(key)?.forEach((ff) => ff());  // 写入时:触发该 key 的所有依赖
}

Step 2:reactive------Proxy 拦截 get/set,串起 track/trigger

js 复制代码
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);                       // 读取 → 收集依赖
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const ok = Reflect.set(target, key, value, receiver);
      trigger(target, key);                     // 写入 → 触发更新
      return ok;
    },
  });
}

Step 3:effect------登记"会读响应式数据的函数"

js 复制代码
function effect(fn) {
  activeEffect = fn;
  fn();           // 立即执行一次,暴露它读了哪些 key(从而被 track 收集)
  activeEffect = null;
  return fn;
}

Step 4:使用与验证

js 复制代码
const state = reactive({ count: 0, name: 'x' });

effect(() => console.log('count=', state.count));   // 立即打印 count= 0,并被收集
state.count = 1;    // 打印 count= 1(自动触发)
state.count = 2;    // 打印 count= 2

// 新增属性也响应(Vue2 做不到的事)
state.name = 'y';   // 触发 name 依赖(如果 effect 读了 name)

这个雏形就是 Vue3 reactive + effect 的骨架。 Vue3 完整实现还加了:Reflect 正确 receiver、effect 的调度与依赖清理(避免无效重复触发)、ref(对基本类型的包装)、computed/watch、以及懒深层代理(first-access 才代理深处对象,优化性能)。

进阶 1:懒深层代理(Vue3 的深层优化)

Vue2 是"初始化时递归整个对象",Vue3 用 Proxy 首次访问才深层代理,省去初始化开销:

js 复制代码
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);
      const value = Reflect.get(target, key, receiver);
      // 关键:如果是对象,就懒地对其做响应式(首次访问才代理,带缓存避免重复)
      if (value && typeof value === 'object') {
        return reactive(value);   // 简化示意;Vue3 有缓存 WeakMap 避免重复代理
      }
      return value;
    },
  });
}

进阶 2:computed(依赖 effect + 缓存)

js 复制代码
function computed(getter) {
  let value;
  let dirty = true;
  effect(() => {        // getter 里读的响应数据变化时,标记 dirty 并重算
    value = getter();
    dirty = false;
  });
  return {
    get value() { return value; },
  };
}
// 使用时 state 变化 → computed 自动重算(缓存已失效则重算)

进阶 3:readonly(只读守卫,状态管理常用)

js 复制代码
const readonly = (obj) => new Proxy(obj, {
  set() { console.warn('只读,不能修改'); return false; },
  deleteProperty() { console.warn('只读'); return false; },
});

进阶 4:真实感受 Vue3 ref 的包装(理解基本类型为何要 ref)

js 复制代码
// reactive 只能代理对象;基本类型(number/string)要包装成对象才能响应
function ref(value) {
  return reactive({ value });   // 极简示意:Vue3 用 get/set + `.value`
}
const count = ref(0);
effect(() => console.log('count=', count.value));
count.value = 5;   // 触发

要点总结

  • Proxy 拦截对象所有操作(读/写/删/遍历/has),是"现代数据劫持"的标准。
  • Reflect 与拦截器一一对应:保 this(receiver)、返回布尔(set 成功与否),必须配合用。
  • 响应式三步 :读取时 track 收集依赖 → 写入时 trigger 触发依赖 → 更新。
  • Vue2 换 Vue3 的原因:新增属性、数组下标/长度在 Vue3 用 Proxy 天然响应,Vue2 做不到。
  • 懒深层代理:首次访问才代理深处对象,省初始化开销(Vue3 优化)。
  • computed/watch/ref 都是建立在 reactive + effect 之上的上层封装。
  • 手写 mini = WeakMap depsMap + Proxy get/set + track/trigger + effect,是所有状态库的钥匙。
  • 应用:响应式状态、只读守卫、字段脱敏、缓存代理、参数校验。

一句话:Proxy 给对象加了"读与写的钩子",Reflect 让钩子操作正确------响应式、脱敏、守卫都是在这两层钩子上做文章。能完整手写一个带懒代理和 computed 的 mini reactive,你就吃透了前端数据流的核心。

相关推荐
Easy_API16 分钟前
OpenAI三周内第二次降价,GPT-5.6 Sol砍了20%到33
大数据·前端·人工智能·gpt·深度学习
深念Y17 分钟前
07-SSR水合问题实战排查与修复记录
前端·vue·vite·nuxt·ssr·csr·水合
前端炒粉19 分钟前
容器预热预跳转方案
前端·vue.js·性能优化
小二李22 分钟前
第12章 nestjs服务端开发:RBAC权限系统设计
java·linux·前端
tt一点通29 分钟前
前端常用设计模式大全
前端·vue.js
张文君34 分钟前
ubuntu26.04坏块分区隔离急速版
java·前端·python
两只羊ovo37 分钟前
给 Agent 装上手脚:手写 mini-Cursor,从建项目到跑起来
前端
Hive_MOM1 小时前
制造企业库存精益化管理数字化:从“库存压资金、找货靠人翻“到“库存水位实时可见、资金周转提速“
前端·制造
光影少年1 小时前
react navite调试方案:Flipper、远程调试
前端·javascript·react native·react.js·前端框架