问题场景
Vue3 的响应式、状态管理库(zustand/valtio)、数据劫持、字段脱敏------现代前端做"数据一变,界面/逻辑自动跟着变",底层全靠 Proxy + Reflect。
很多人在用 Vue3 时只知道"ref/reactive 会让数据响应式",但遇到这几个问题就懵:
- 为什么
reactive里的对象要深层遍历?新增属性为什么自动响应? - 为什么数组下标/长度修改在 Vue3 能响应、Vue2 却不行?
computed、watch底层是怎么"感知"到依赖变化的?- Proxy 里的
get为什么不直接return obj[key],非要包一层Reflect.get?
理解这些,你不仅能用好 Vue3,还能手写 mini 响应式,一眼看穿 zustand 等库的底层。本文从零手写一个完整可用的 mini 响应式系统。
原理深入
1. Proxy:给对象加"读写的钩子"
Proxy 用 new 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,你就吃透了前端数据流的核心。