深入理解Proxy

前言

Proxy 是 ES6 引入的元编程利器,它能拦截并自定义对象的基本操作,从属性读写到函数调用再到 in 运算符,几乎无所不能。Vue3 的响应式系统正是基于 Proxy 实现的。本文将全面拆解 Proxy 的原理和实际应用。

Proxy 基础

什么是 Proxy

Proxy 在目标对象之前架设一层”拦截”,外界对该对象的访问都必须先通过这层拦截:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const target = { name: 'hexo' }
const handler = {
get(target, key, receiver) {
console.log(`读取属性: ${String(key)}`)
return target[key]
},
set(target, key, value, receiver) {
console.log(`设置属性: ${String(key)} = ${value}`)
target[key] = value
return true
}
}
const proxy = new Proxy(target, handler)

proxy.name // 读取属性: name → "hexo"
proxy.age = 25 // 设置属性: age = 25

关键点:对 proxy 进行操作才会触发拦截,直接操作原始对象 target 不会。

Reflect 的重要性

Reflect 是搭配 Proxy 的最佳拍档,它提供了一套与 Proxy 捕获器一一对应的静态方法:

1
2
3
4
5
6
7
8
const proxy = new Proxy(target, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver) // 推荐写法
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
}
})

使用 Reflect 的三个理由:

  1. 保持 this 绑定正确 — receiver 参数确保 getter 中的 this 指向 proxy
  2. 返回值合理 — Reflect.set 返回 true/false,而直接赋值可能静默失败
  3. 代码语义统一 — 不再混用 delete obj[key]、key in obj 等操作符语法

13 种捕获器详解

Proxy 共支持 13 种捕获器,覆盖了对对象的所有操作。

属性相关

捕获器 拦截操作 触发场景
get(target, key, receiver) 读取属性 proxy.key、proxy[key]
set(target, key, value, receiver) 设置属性 proxy.key = val
has(target, key) in 运算符 'key' in proxy
deleteProperty(target, key) delete delete proxy.key
getOwnPropertyDescriptor(target, key) 属性描述符 Object.getOwnPropertyDescriptor()

对象结构相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const proxy = new Proxy(target, {
ownKeys(target) {
// 拦截 Object.keys()、for...in、Object.getOwnPropertyNames() 等
return Reflect.ownKeys(target).filter(k => k !== 'password')
},
defineProperty(target, key, descriptor) {
// 拦截 Object.defineProperty() 和属性赋值
return Reflect.defineProperty(target, key, descriptor)
},
isExtensible(target) {
// 拦截 Object.isExtensible()
return Reflect.isExtensible(target)
},
preventExtensions(target) {
// 拦截 Object.preventExtensions()
return Reflect.preventExtensions(target)
}
})

原型与函数相关

捕获器 拦截操作
getPrototypeOf(target) Object.getPrototypeOf()、__proto__
setPrototypeOf(target, proto) Object.setPrototypeOf()
apply(target, thisArg, args) proxy(...args)、proxy.call()、proxy.apply()
construct(target, args, newTarget) new proxy(...args)

apply 和 construct 只在 Proxy 代理的是函数时生效。

实战场景

场景一:数据验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createValidator(schema) {
return new Proxy({}, {
set(target, key, value) {
const rule = schema[key]
if (rule && !rule.test(value)) {
throw new Error(`${String(key)}: ${rule.message}`)
}
return Reflect.set(target, key, value)
}
})
}

const user = createValidator({
age: { test: v => v >= 0 && v <= 120, message: '年龄需在 0-120 之间' },
name: { test: v => v.length >= 2, message: '姓名至少 2 个字符' }
})

user.name = '张三' // OK
user.age = 150 // Error: 年龄需在 0-120 之间

场景二:负索引数组

1
2
3
4
5
6
7
8
9
10
11
12
13
function createSmartArray(arr) {
return new Proxy(arr, {
get(target, key, receiver) {
let index = Number(key)
if (index < 0) index = target.length + index
return Reflect.get(target, String(index), receiver)
}
})
}

const arr = createSmartArray(['a', 'b', 'c', 'd'])
console.log(arr[-1]) // 'd'
console.log(arr[-2]) // 'c'

场景三:观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function observe(target, onChange) {
return new Proxy(target, {
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
onChange({ key, oldValue, newValue: value })
}
return result
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
onChange({ key, type: 'delete' })
}
return result
}
})
}

const state = observe({ count: 0 }, (change) => {
console.log('数据变更:', change)
})

state.count++ // 数据变更: { key: 'count', oldValue: 0, newValue: 1 }

场景四:API 缓存代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function memoize(fn, ttl = 5000) {
const cache = new Map()
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args)
const cached = cache.get(key)
if (cached && Date.now() - cached.time < ttl) {
console.log('命中缓存')
return cached.value
}
const result = Reflect.apply(target, thisArg, args)
cache.set(key, { value: result, time: Date.now() })
return result
}
})
}

Vue3 中 Proxy 的应用

Vue3 的响应式系统完全基于 Proxy 构建,这里拆解关键实现。

1. reactive 核心实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Vue3 reactive 简化源码
function reactive(target) {
if (typeof target !== 'object' || target === null) return target

return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
// 依赖收集
track(target, key)
// 懒代理:访问到嵌套对象时才递归
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},

set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
// 派发更新
trigger(target, key)
}
return result
},

deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
trigger(target, key) // 删除属性也能触发更新
}
return result
},

has(target, key) {
// 拦截 in 运算符,用于 v-if 等指令
track(target, key)
return Reflect.has(target, key)
},

ownKeys(target) {
// 拦截 Object.keys() 和 for...in
track(target, Symbol('iterate'))
return Reflect.ownKeys(target)
}
})
}

2. Vue3 响应式数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
// 完整数据流示意
const state = reactive({ count: 0, info: { name: 'Vue3' } })

effect(() => {
// 首次执行:读取 state.count → get 拦截 → track → 收集当前 effect
console.log(state.count)
})

state.count++ // set 拦截 → trigger → 重新执行 effect

// 嵌套对象懒代理
state.info // get 拦截 → 返回 reactive({ name: 'Vue3' })
state.info.name // 读写都会拦截

三级依赖存储结构:

层级 结构 键 值
targetMap WeakMap target 对象 depsMap
depsMap Map 属性名 key dep (Set)
dep Set — effect 函数

使用 WeakMap 存储 target → deps 映射,当原对象被销毁时依赖自动 GC,避免内存泄漏。

3. ref 也依赖 Proxy(对象类型时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ref(value) {
// 如果是对象,内部转成 reactive
if (typeof value === 'object') {
return reactive({ value })
}
// 基本类型用 getter/setter
return {
get value() { track(this, 'value'); return value },
set value(v) { value = v; trigger(this, 'value') }
}
}

const obj = ref({ name: 'hello' })
obj.value.name = 'world' // ref 对象时走 Proxy 拦截

4. shallowReactive 和 readonly 的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// shallowReactive — 只代理第一层
function shallowReactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver) // 不递归
}
// set 同上
})
}

// readonly — 只读代理(给 props 用)
function readonly(target) {
return new Proxy(target, {
set() { return true /* 静默忽略 */ },
deleteProperty() { return true }
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { reactive, shallowReactive, readonly } from 'vue'

// 深层响应,适合复杂表单
const form = reactive({ name: '', addr: { city: '' } })

// 浅层响应,适合大量数据只改引用
const list = shallowReactive([...bigData])

// props 本质是 readonly 的 reactive
const props = defineProps({ title: String })
// props.title = 'new' → 静默失败,不会触发更新
</script>

5. toRaw / markRaw 绕过 Proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { reactive, toRaw, markRaw } from 'vue'

const obj = reactive({ id: 1, name: 'test' })

// toRaw 拿到 Proxy 背后的原始对象
const raw = toRaw(obj)
raw.name = 'changed' // 不会触发更新

// markRaw 标记对象永不代理(如第三方实例、大体积数据)
const staticData = markRaw({ huge: '...' })
const state = reactive({ data: staticData })
// state.data 不会被代理,减少性能开销
</script>

注意事项

Proxy 无法代理的情况

  1. 原始值不能直接代理 — 必须包装为对象(这也是 Vue3 中 ref 存在的意义之一)
  2. Map、Set 等内置对象 — 代理时需要特殊处理,否则内部 this 会指错
  3. 私有属性 # — Proxy 无法拦截私有字段的访问

性能考量

操作 无代理 有代理 开销
读属性 ~0.2ns ~5ns 约 25x
写属性 ~0.3ns ~8ns 约 27x

代价虽比直接访问高,但绝对速度仍然极快,正常业务场景无需担心。避免在高频循环中对同一个对象反复创建 Proxy 即可。

总结

  • Proxy 提供了 13 种捕获器,可以拦截几乎所有的对象底层操作
  • Reflect 是 Proxy 的最佳搭档,确保行为正确和代码一致
  • 实际应用场景包括:数据验证、响应式系统、日志埋点、缓存代理等
  • Vue3 响应式正是利用 Proxy 全面代替了 Object.defineProperty
  • 注意内置对象代理的兼容性问题,避免在高频场景反复创建代理

掌握 Proxy 就掌握了现代前端框架响应式系统的底层密码。


内容由 AI 生成,仅供参考。本文发布于 码上学习,转载请注明出处。