第 4 章

Proxy 与 Reflect:13 种 trap 的完整语义与响应式基础设施

第4章:Proxy 与 Reflect——13 种 trap 的完整语义与响应式基础设施

ECMAScript 规范中,所有对象操作最终都归结为 13 个"内部方法"的调用。Proxy 的设计正是拦截这 13 个内部方法,而不是 JavaScript 的语法层面——这意味着无论代码多么复杂,对对象的所有操作都可以被精确拦截。

本章核心问题:Proxy 的 13 个 trap 各自拦截什么操作?为什么 Reflect 必须配套使用?Vue 3 如何基于这套机制构建响应式系统?

读完本章你将理解


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.keyobj[key]
[[Set]] obj.key = valueobj[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.keyobj['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 时:

  1. child 自身没有 value 属性,沿原型链查找
  2. 找到 parentProxy,触发 get trap
  3. get trap 执行 target[key],即 parent['value']
  4. 执行 parentvalue getter,此时 thisparent(不是 child
  5. parent._valueundefined_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 拦截的是对象的属性访问,但 MapSet 的数据不是通过属性访问的,而是通过方法调用(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.keyobj[key]Reflect.get() 任意值 track 依赖
set obj.key = vReflect.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 的实现

数组的 includesindexOflastIndexOf 在原始实现中不会触发响应式追踪,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。但修改是真实发生的(rawstate 指向同一份数据),下次通过 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。这是精确追踪的正确行为,避免了不必要的重新渲染。


本章小结

  1. Proxy 的 13 个 trap 对应 ECMAScript 的 13 个内部方法,而不是 JavaScript 的语法结构。这意味着无论用什么语法(obj.keyobj['key']Reflect.get()),都会触发相同的 trap。拦截是语义层面的,不是语法层面的。

  2. Reflect 是 Proxy trap 的配套设计,核心价值在于 receiver 参数。在继承场景下,Reflect.get(target, key, receiver) 确保 getter 内的 this 绑定到原始调用者(receiver)而不是被代理对象(target),解决了 target[key] 在原型链中 this 绑定错误的问题。

  3. Vue 3 对普通对象和集合类型使用不同的 Proxy handlermutableHandlers 用于普通对象和数组,直接用 get/set/deleteProperty/has/ownKeys 这 5 个 trap;collectionHandlers 用于 Map/Set/WeakMap/WeakSet,因为这些类型的数据通过方法调用访问,需要在 get trap 中拦截方法并包装,同时把 this 绑定回原始对象。

  4. 嵌套对象的懒代理是 Vue 3 初始化性能提升 55% 的关键之一:调用 reactive() 时只代理顶层对象,嵌套对象在实际访问时才创建 Proxy。配合 WeakMap 缓存,确保同一对象只创建一个 Proxy 实例。

  5. Proxy 有不变量约束,不能任意返回值:non-configurable non-writable 属性的 get 必须返回原始值,不可扩展对象的 ownKeys 必须包含所有已有 key 等规则由 ECMAScript 规范强制,违反会抛出 TypeError。理解这些不变量有助于在实现自定义 Proxy 时避免难以调试的错误。

本章评分
4.5  / 5  (76 评分)

💬 留言讨论