Proxy 与 Reflect:13 种 trap 的完整语义与响应式基础设施
第4章:Proxy 与 Reflect——13 种 trap 的完整语义与响应式基础设施
ECMAScript 规范中,所有对象操作最终都归结为 13 个"内部方法"的调用。Proxy 的设计正是拦截这 13 个内部方法,而不是 JavaScript 的语法层面——这意味着无论代码多么复杂,对对象的所有操作都可以被精确拦截。
本章核心问题:Proxy 的 13 个 trap 各自拦截什么操作?为什么 Reflect 必须配套使用?Vue 3 如何基于这套机制构建响应式系统?
读完本章你将理解:
- 13 种 trap 的完整语义,每种对应的 JavaScript 操作
- Reflect 的设计动机:Receiver 参数如何解决继承场景下的 this 绑定问题
- Vue 3 响应式系统中 Proxy handler 的两套方案:普通对象 vs Map/Set/WeakMap/WeakSet
Level 1 · 你需要知道的(1-3年经验)
4.1 ECMAScript 的 13 个内部方法
在 JavaScript 引擎的视角里,每个对象都实现了 ECMAScript 规范定义的内部方法(Internal Methods)。这些内部方法不是 JavaScript 代码可以直接调用的,而是引擎在处理各种 JavaScript 操作时内部使用的机制。
| 内部方法 | 对应的 JavaScript 操作 |
|---|---|
[[GetPrototypeOf]] |
Object.getPrototypeOf(obj) 或 obj.__proto__ |
[[SetPrototypeOf]] |
Object.setPrototypeOf(obj, proto) |
[[IsExtensible]] |
Object.isExtensible(obj) |
[[PreventExtensions]] |
Object.preventExtensions(obj) |
[[GetOwnProperty]] |
Object.getOwnPropertyDescriptor(obj, key) |
[[DefineOwnProperty]] |
Object.defineProperty(obj, key, desc) |
[[HasProperty]] |
key in obj |
[[Get]] |
obj.key 或 obj[key] |
[[Set]] |
obj.key = value 或 obj[key] = value |
[[Delete]] |
delete obj.key |
[[OwnPropertyKeys]] |
Object.keys(obj)、Object.getOwnPropertyNames(obj) |
[[Call]] |
func() 或 func.call() |
[[Construct]] |
new func() |
Proxy 的 13 个 trap 与这 13 个内部方法一一对应:
const handler = {
getPrototypeOf(target) {}, // [[GetPrototypeOf]]
setPrototypeOf(target, proto) {}, // [[SetPrototypeOf]]
isExtensible(target) {}, // [[IsExtensible]]
preventExtensions(target) {}, // [[PreventExtensions]]
getOwnPropertyDescriptor(target, key) {}, // [[GetOwnProperty]]
defineProperty(target, key, desc) {}, // [[DefineOwnProperty]]
has(target, key) {}, // [[HasProperty]]
get(target, key, receiver) {}, // [[Get]]
set(target, key, value, receiver) {}, // [[Set]]
deleteProperty(target, key) {}, // [[Delete]]
ownKeys(target) {}, // [[OwnPropertyKeys]]
apply(target, thisArg, args) {}, // [[Call]]
construct(target, args, newTarget) {} // [[Construct]]
};
这个对应关系不是巧合,而是 Proxy 的设计哲学:在 JavaScript 语义层面,而不是语法层面进行拦截。无论你用什么语法访问对象属性(obj.key、obj['key']、Reflect.get(obj, 'key')),最终都会触发 [[Get]] 内部方法,也就触发了 get trap。
4.2 最常用的 5 个 trap 详解
get(target, key, receiver)
const handler = {
get(target, key, receiver) {
console.log(`读取 ${String(key)}`);
return Reflect.get(target, key, receiver);
}
};
const obj = new Proxy({ name: 'Vue' }, handler);
// 触发 get trap 的所有方式
obj.name; // "读取 name"
obj['name']; // "读取 name"
'name' in obj; // 不触发 get(触发 has trap)
Object.keys(obj); // 不触发 get(触发 ownKeys trap)
set(target, key, value, receiver)
const handler = {
set(target, key, value, receiver) {
console.log(`设置 ${String(key)} = ${value}`);
const result = Reflect.set(target, key, value, receiver);
return result; // 必须返回 true/false,表示设置是否成功
}
};
has(target, key)
const handler = {
has(target, key) {
console.log(`检查 ${String(key)} 是否存在`);
return key in target;
}
};
const obj = new Proxy({}, handler);
'name' in obj; // 触发 has trap:"检查 name 是否存在"
deleteProperty(target, key)
const handler = {
deleteProperty(target, key) {
console.log(`删除 ${String(key)}`);
return Reflect.deleteProperty(target, key);
}
};
const obj = new Proxy({ name: 'Vue' }, handler);
delete obj.name; // 触发 deleteProperty trap
ownKeys(target)
const handler = {
ownKeys(target) {
console.log('枚举 ownKeys');
return Reflect.ownKeys(target);
}
};
const obj = new Proxy({ a: 1, b: 2 }, handler);
Object.keys(obj); // 触发 ownKeys
Object.getOwnPropertyNames(obj); // 触发 ownKeys
Object.getOwnPropertySymbols(obj); // 触发 ownKeys
for (const key in obj) { /* ... */ } // 触发 ownKeys(还触发 has)
4.3 为什么需要 Reflect?
你可能注意到上面所有的 trap 实现都在调用 Reflect.get(target, key, receiver) 而不是直接用 target[key]。这个区别非常重要,在继承场景下会产生根本性的行为差异。
错误示范(不用 Reflect):
const parent = {
get value() {
return this._value;
}
};
const parentProxy = new Proxy(parent, {
get(target, key) {
return target[key]; // 错误!this 绑定错误
}
});
const child = Object.create(parentProxy);
child._value = 42;
console.log(child.value); // 返回 undefined,不是 42!
为什么返回 undefined?当访问 child.value 时:
child自身没有value属性,沿原型链查找- 找到
parentProxy,触发gettrap gettrap 执行target[key],即parent['value']- 执行
parent的valuegetter,此时this是parent(不是child) parent._value是undefined(_value定义在child上,不是parent上)
正确写法(使用 Reflect):
const parentProxy = new Proxy(parent, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver); // receiver = child(原始调用者)
}
});
console.log(child.value); // 正确返回 42
Reflect.get(target, key, receiver) 的 receiver 参数:在 getter 内部,this 会被绑定为 receiver 而不是 target。这保证了 getter 里的 this 始终指向原始的调用者(child),而不是被代理的对象(parent)。
Reflect 的 receiver 参数作用:
child.value 的访问链:
─────────────────────────────────────────────────────────
child ──原型链──► parentProxy ──trap──► parent.value getter
不用 Reflect 时(错误):
this = target = parent → parent._value = undefined ✗
用 Reflect 时(正确):
this = receiver = child → child._value = 42 ✓
─────────────────────────────────────────────────────────
4.4 Map/Set 的特殊处理
Proxy 拦截的是对象的属性访问,但 Map 和 Set 的数据不是通过属性访问的,而是通过方法调用(map.get(key)、map.set(key, value)、set.add(value) 等)。这意味着:
const map = new Map([['key', 'value']]);
const proxy = new Proxy(map, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
}
});
proxy.get('key'); // TypeError: Method Map.prototype.get called on incompatible receiver
为什么报错?proxy.get 首先触发 get trap,返回 Map.prototype.get 方法。然后用 proxy 作为 this 调用这个方法。但 Map.prototype.get 内部使用了 [[MapData]] 内部槽(internal slot),这个内部槽只存在于真实的 Map 实例上,不存在于 Proxy 上。
Vue 3 解决这个问题的方式是为集合类型提供专门的 handler,在 get trap 中把 this 绑定回原始对象:
// Vue 3 集合 handler 的核心思路(简化)
const collectionHandlers = {
get(target, key, receiver) {
// 对集合方法,返回绑定了原始 target 的版本
if (key === 'get') {
return function(mapKey) {
track(target, TrackOpTypes.GET, mapKey);
return target.get(mapKey); // 用 target 而不是 proxy 调用
};
}
if (key === 'set') {
return function(mapKey, value) {
const hadKey = target.has(mapKey);
const result = target.set(mapKey, value);
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, mapKey, value);
} else {
trigger(target, TriggerOpTypes.SET, mapKey, value);
}
return result;
};
}
// ... 其他方法
}
};
Level 2 · 它是怎么运行的(3-5年经验)
4.5 13 种 trap 的完整语义表
| Trap | 拦截的操作 | 返回值要求 | Vue 3 中的用途 |
|---|---|---|---|
get |
obj.key、obj[key]、Reflect.get() |
任意值 | track 依赖 |
set |
obj.key = v、Reflect.set() |
Boolean | trigger 更新 |
has |
key in obj |
Boolean | track(v-if 等场景) |
deleteProperty |
delete obj.key |
Boolean | trigger 删除事件 |
ownKeys |
Object.keys()、for...in 等 |
字符串/Symbol 数组 | track ownKeys |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor() |
属性描述符或 undefined | 较少使用 |
defineProperty |
Object.defineProperty() |
Boolean | 较少使用 |
getPrototypeOf |
Object.getPrototypeOf()、instanceof |
对象或 null | 较少使用 |
setPrototypeOf |
Object.setPrototypeOf() |
Boolean | 较少使用 |
isExtensible |
Object.isExtensible() |
Boolean | 较少使用 |
preventExtensions |
Object.preventExtensions() |
Boolean | 较少使用 |
apply |
func()、func.call()、func.apply() |
任意值 | 不用于对象代理 |
construct |
new func() |
对象 | 不用于对象代理 |
4.6 Vue 3 的两套 Proxy Handler
Vue 3 的响应式系统对两类对象使用不同的 handler:
响应式对象分类:
reactive(obj)
│
├── 普通对象/数组 → mutableHandlers
│ ┌──────────────────────────────┐
│ │ get → track │
│ │ set → trigger │
│ │ deleteProperty → trigger │
│ │ has → track │
│ │ ownKeys → track │
│ └──────────────────────────────┘
│
└── Map/Set/WeakMap/WeakSet → mutableCollectionHandlers
┌──────────────────────────────┐
│ get → 返回包装后的方法 │
│ .get() → track + 原始 get │
│ .set() → 原始 set + trigger │
│ .has() → track + 原始 has │
│ .add() → 原始 add + trigger │
│ .delete() → 原始 delete + trigger │
│ .clear() → 原始 clear + trigger │
│ .forEach() → track + 原始 forEach │
│ .size → track + 原始 size │
│ [Symbol.iterator] → track │
└──────────────────────────────┘
4.7 嵌套对象的懒代理机制
Vue 3 不会在 reactive() 调用时递归代理所有嵌套对象,而是采用懒代理策略:
const raw = {
user: {
profile: {
name: 'Vue',
settings: {
theme: 'dark'
}
}
}
};
const state = reactive(raw);
// 此时:只有 raw 本身被代理,raw.user、raw.user.profile、raw.user.profile.settings 都没有被代理
const user = state.user;
// 此时:触发 get trap,发现 raw.user 是对象,对它创建 Proxy 并返回
// 现在 raw.user 被代理了
const profile = state.user.profile;
// 此时:先访问 state.user(触发 get,返回 raw.user 的 Proxy)
// 再访问 .profile(触发 get,发现 raw.user.profile 是对象,对它创建 Proxy 并返回)
// 现在 raw.user.profile 也被代理了
这个机制的内存优化效果:
深层嵌套对象,初始访问时:
raw(被代理)
└── .user(未代理)
└── .profile(未代理)
└── .settings(未代理)
只有在模板/代码中访问到某层时,该层才会被代理:
如果模板只访问 state.user.profile.name:
raw(被代理)
└── .user(被代理) ← 访问 state.user 时创建
└── .profile(被代理) ← 访问 state.user.profile 时创建
└── .settings(未代理!) ← 没有访问,不会代理
Vue 3 还维护了两个 WeakMap 来避免重复代理:
// packages/reactivity/src/reactive.ts
const reactiveMap = new WeakMap<Target, any>() // 缓存 reactive 结果
const shallowReactiveMap = new WeakMap<Target, any>()
const readonlyMap = new WeakMap<Target, any>()
const shallowReadonlyMap = new WeakMap<Target, any>()
function reactive(target: object) {
// 已经是 readonly proxy,直接返回
if (isReadonly(target)) {
return target;
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap // 用于缓存
);
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 检查缓存:同一个 target 已经被代理过
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy; // 直接返回缓存的 Proxy
}
const proxy = new Proxy(target, /* ... handlers */);
proxyMap.set(target, proxy); // 缓存
return proxy;
}
这确保了:对同一个对象多次调用 reactive(),返回的是同一个 Proxy 实例。
4.8 Proxy 的不变量(Invariants)
Proxy 的 trap 必须遵守 ECMAScript 规范中定义的不变量(invariants)。如果 trap 的返回值违反了这些规则,JavaScript 引擎会抛出 TypeError:
不变量示例:
// 不变量1:getPrototypeOf 必须返回对象或 null
const proxy = new Proxy({}, {
getPrototypeOf() {
return 42; // TypeError!违反不变量
}
});
// 不变量2:如果目标对象不可扩展,ownKeys 必须包含目标的所有 own keys
const nonExtensible = Object.preventExtensions({ a: 1 });
const proxy2 = new Proxy(nonExtensible, {
ownKeys() {
return ['b']; // TypeError!违反不变量('a' 必须在结果中)
}
});
// 不变量3:如果 target.key 是 non-configurable non-writable,get 必须返回原始值
const obj = {};
Object.defineProperty(obj, 'fixed', {
value: 42,
configurable: false,
writable: false
});
const proxy3 = new Proxy(obj, {
get(target, key) {
if (key === 'fixed') return 99; // TypeError!违反不变量
return Reflect.get(target, key);
}
});
这些不变量保证了 Proxy 无法违反 JavaScript 的基本对象语义,防止代码通过 Proxy 欺骗引擎。
Level 3 · 设计文档与源码(资深开发者)
4.9 Vue 3 中 mutableHandlers 的完整实现
// packages/reactivity/src/baseHandlers.ts(核心部分)
// get trap 的工厂函数
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
// 处理特殊内部 key(ReactiveFlags)
if (key === ReactiveFlags.IS_REACTIVE) return !isReadonly
if (key === ReactiveFlags.IS_READONLY) return isReadonly
if (key === ReactiveFlags.IS_SHALLOW) return shallow
if (key === ReactiveFlags.RAW) {
// toRaw() 的实现原理:通过访问 __v_raw 获取原始对象
if (receiver === (isReadonly ? (shallow ? shallowReadonlyMap : readonlyMap)
: (shallow ? shallowReactiveMap : reactiveMap)).get(target)
) {
return target
}
return
}
const targetIsArray = isArray(target)
if (!isReadonly) {
// 数组的特殊方法(includes、indexOf、lastIndexOf 需要追踪每个索引)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
}
const res = Reflect.get(target, key, receiver)
// 跳过内置 Symbol 的追踪(Symbol.iterator, Symbol.toPrimitive 等)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
// 追踪依赖
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// shallowReactive:不递归代理
if (shallow) return res
// ref 自动解包(数组中的 ref 不解包)
if (isRef(res)) {
return targetIsArray && isIntegerKey(key) ? res : res.value
}
// 懒代理:嵌套对象在访问时才创建 Proxy
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
// set trap 的实现
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (!shallow) {
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
// 获取 raw 值,避免响应式对象之间的嵌套影响
oldValue = toRaw(oldValue)
value = toRaw(value)
}
// 重要:如果 target 不是数组,且旧值是 ref,新值不是 ref
// 直接更新 ref.value(保持 ref 的响应性)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false
} else {
oldValue.value = value
return true
}
}
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length // 数组:检查是否在范围内
: hasOwn(target, key) // 对象:检查 key 是否已存在
const result = Reflect.set(target, key, value, receiver)
// 关键:只在 target 是 receiver 的原始对象时触发
// 这防止了原型链中的代理重复触发
if (target === toRaw(receiver)) {
if (!hadKey) {
// 新增属性:触发 ADD 操作
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 修改属性:触发 SET 操作(值有变化时)
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
4.10 集合类型 handler 的源码分析
// packages/reactivity/src/collectionHandlers.ts(核心部分)
// Map.prototype.get 的响应式包装
function get(this: MapTypes, key: unknown, isReadonly = false, isShallow = false) {
const target = (this as any)[ReactiveFlags.RAW] // 获取原始 Map
const rawKey = toRaw(key) // 获取 key 的原始值
if (key !== rawKey) {
// 如果 key 本身是响应式的,追踪两个版本
!isReadonly && track(target, TrackOpTypes.GET, key)
}
!isReadonly && track(target, TrackOpTypes.GET, rawKey)
const { has } = getProto(target)
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(target, key)) {
return wrap(target.get(key)) // 嵌套对象也要响应式化
} else if (has.call(target, rawKey)) {
return wrap(target.get(rawKey))
} else if (target !== this) {
// 在只读 map 的 set 中查找
target.get(key)
}
}
// Map.prototype.set 的响应式包装
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value) // 新增 key
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue) // 修改 key
}
return this
}
4.11 arrayInstrumentations 的实现
数组的 includes、indexOf、lastIndexOf 在原始实现中不会触发响应式追踪,Vue 3 需要对它们进行特殊处理:
// packages/reactivity/src/baseHandlers.ts
const arrayInstrumentations: Record<string, Function> = {}
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) // 获取原始数组
// 遍历数组,追踪每个索引的依赖
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// 先用原始方法搜索
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// 搜索失败,尝试用 raw 版本的参数再搜索一次
// (参数可能本身是响应式对象,需要用 raw 版本比较)
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
// 会修改数组长度的方法也需要特殊处理
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
pauseTracking() // 暂停依赖追踪(避免在修改时产生追踪循环)
const res = method.apply(this, args)
resetTracking() // 恢复依赖追踪
return res
}
})
Level 4 · 边界与陷阱(全体适用)
陷阱1:in 操作符触发 has trap,影响响应式追踪
import { reactive, watchEffect } from 'vue';
const state = reactive({ name: 'Vue', age: 18 });
watchEffect(() => {
// 这里使用 in 操作符
if ('name' in state) {
console.log('有 name 属性');
}
});
// 添加新属性
state.newProp = 'hello';
// watchEffect 重新执行!虽然我们只检查了 'name'
// 为什么?in 操作符触发 has trap,has trap 追踪的是 ownKeys
// 当新属性被添加时,ownKeys 发生变化,所有追踪了 ownKeys 的 effect 都会重新执行
根本原因:has trap 在 Vue 3 中会追踪该操作,且当对象有新属性添加时会触发。这是正确的行为(你需要知道某个 key 是否存在,当新 key 出现时应该重新检查),但可能导致意外的重新渲染。
陷阱2:toRaw 之后的对象操作不触发响应式
import { reactive, toRaw, watchEffect } from 'vue';
const state = reactive({ count: 0 });
const raw = toRaw(state);
watchEffect(() => {
console.log(state.count); // 追踪 state.count
});
// 正确:通过 proxy 修改,触发更新
state.count++; // 触发 watchEffect
// 错误:通过 raw 修改,不触发更新
raw.count++; // watchEffect 不执行!
// 但 raw.count 确实变了,只是没有通知到响应式系统
console.log(raw.count); // 2
console.log(state.count); // 也是 2(因为 raw 是原始对象,state 是它的代理)
根本原因:toRaw 返回 Proxy 后面的原始对象。对原始对象的修改不经过 Proxy,不会触发 set trap,也就不会触发 trigger,不会通知任何 effect。但修改是真实发生的(raw 和 state 指向同一份数据),下次通过 state 访问时会看到更新后的值。
陷阱3:Proxy 无法代理不可扩展对象的新属性
import { reactive } from 'vue';
const obj = Object.freeze({ count: 0 });
const state = reactive(obj); // 创建 Proxy,但修改会静默失败
// 在严格模式下会抛出 TypeError
// 在非严格模式下修改被静默忽略
state.count = 1; // 在非严格模式:静默失败,state.count 仍然是 0
state.newProp = 1; // 同样静默失败
Vue 会在 DEV 模式下对 frozen 对象打印警告:
[Vue warn]: Set operation on key "count" failed: target is readonly.
实际场景:从服务器收到的 JSON 对象通常没有问题,但如果你对收到的数据进行了 Object.freeze()(某些库会这样做以防止意外修改),再传给 reactive() 就会出现这个问题。
陷阱4:Map/Set 的响应式追踪只追踪访问的 key
import { reactive, watchEffect } from 'vue';
const map = reactive(new Map([['a', 1], ['b', 2]]));
watchEffect(() => {
console.log(map.get('a')); // 只追踪 key 'a'
});
map.set('b', 99); // 不触发!watchEffect 只追踪了 'a','b' 的变化无关
map.set('a', 99); // 触发!追踪了 'a'
map.delete('a'); // 触发!'a' 被删除了
// 特殊情况:追踪 size 的 effect 会在任何 key 变化时触发
watchEffect(() => {
console.log(map.size); // 追踪 size
});
map.set('c', 3); // 触发!size 变化了(2 → 3)
map.set('a', 99); // 不触发!size 没变(仍然是 3,因为 'a' 已经存在)
根本原因:Vue 3 的 Map 响应式追踪精确到 key 级别。map.get('a') 只追踪 key 'a'。对其他 key 的操作不触发这个 effect。这是精确追踪的正确行为,避免了不必要的重新渲染。
本章小结
-
Proxy 的 13 个 trap 对应 ECMAScript 的 13 个内部方法,而不是 JavaScript 的语法结构。这意味着无论用什么语法(
obj.key、obj['key']、Reflect.get()),都会触发相同的 trap。拦截是语义层面的,不是语法层面的。 -
Reflect 是 Proxy trap 的配套设计,核心价值在于
receiver参数。在继承场景下,Reflect.get(target, key, receiver)确保 getter 内的this绑定到原始调用者(receiver)而不是被代理对象(target),解决了target[key]在原型链中 this 绑定错误的问题。 -
Vue 3 对普通对象和集合类型使用不同的 Proxy handler:
mutableHandlers用于普通对象和数组,直接用 get/set/deleteProperty/has/ownKeys 这 5 个 trap;collectionHandlers用于 Map/Set/WeakMap/WeakSet,因为这些类型的数据通过方法调用访问,需要在 get trap 中拦截方法并包装,同时把this绑定回原始对象。 -
嵌套对象的懒代理是 Vue 3 初始化性能提升 55% 的关键之一:调用
reactive()时只代理顶层对象,嵌套对象在实际访问时才创建 Proxy。配合 WeakMap 缓存,确保同一对象只创建一个 Proxy 实例。 -
Proxy 有不变量约束,不能任意返回值:non-configurable non-writable 属性的 get 必须返回原始值,不可扩展对象的 ownKeys 必须包含所有已有 key 等规则由 ECMAScript 规范强制,违反会抛出 TypeError。理解这些不变量有助于在实现自定义 Proxy 时避免难以调试的错误。