众所周知,在Vue3中,用Proxy代理代替了原有的
Object.defineProperty,接下来我用手写数据响应的方式,让大家再次深刻的理解一下Proxy究竟是什么样的"Magic" 。
一、先搞懂:什么是「数据响应式」?
简单说:当数据发生变化时,页面(或逻辑)自动做出反应,不需要我们手动去更新。
举个生活例子:
- 你在购物 APP 上把商品加入购物车(数据变化:购物车列表新增商品)
- 购物车图标上的数字自动从 0 变成 1(页面自动反应)
- 这个 "数据变 → 页面自动变" 的过程,就是数据响应式。
在代码里,没有响应式时,我们要手动做所有操作;有了响应式,数据变化会 "自动触发" 后续逻辑,这就是 Proxy 的核心价值。 大家发现了吗,特点就是:检测数据变化->触发数据更新->视图渲染 这三个步骤。
二、先认识两个核心工具:Proxy 和 Reflect
这两个是 ES6 新增的 API,也是实现响应式的基础,我们先逐个搞懂,不着急组合。
Proxy:给对象加一个 "监听器 / 拦截器"
你可以把 Proxy 理解为:给普通对象套了一个 "保护壳",任何对对象的「读取」「修改」操作,都必须先经过这个保护壳,我们可以在壳里监听 / 拦截这些操作。
Proxy 的基本语法
js
const person = {
name: "张三",
age: 20
};
// 创建 Proxy 代理对象(给 person 套上"监听器")
const proxyPerson = new Proxy(person, {
// 【拦截读取操作】:当读取 proxyPerson 的属性时,自动触发这个 get 方法
get(target, property) {
// target:被代理的原始对象(这里就是 person)
// property:当前要读取的属性名(比如读取 proxyPerson.name 时,property 就是 "name")
console.log(`有人读取了 ${property} 属性`);
// 返回属性值(不让读取操作被拦截后"丢失")
return target[property];
},
// 【拦截修改操作】:当修改 proxyPerson 的属性时,自动触发这个 set 方法
set(target, property, value) {
// target:被代理的原始对象(person)
// property:当前要修改的属性名(比如修改 proxyPerson.age 时,property 就是 "age")
// value:当前要修改的新值(比如 proxyPerson.age = 21,value 就是 21)
console.log(`有人修改了 ${property} 属性,新值是 ${value}`);
// 执行真正的修改操作(把新值赋给原始对象的对应属性)
target[property] = value;
// 注意:set 方法最好返回 true,表示修改成功(保持原生行为)
return true;
}
});
测试一下 Proxy 的效果
我们不直接操作原始对象 person,而是操作代理对象 proxyPerson:
js
console.log(proxyPerson.name); //查看代理后name属性的下的值
proxyPerson.age = 20; //更改属性age的值
console.log(proxyPerson.age); //输出更改属性age后的值
你将会看到

强调
- 代理对象(
proxyPerson)和原始对象(person)是 "关联" 的,但不是同一个对象 - 必须操作代理对象才会触发拦截器,直接操作原始对象不会有任何反应:
像下面这样是无效的

Reflect:更 "友好" 的对象操作工具
刚才我们在 Proxy 的 get/set 里,用了 target[property] 读取、target[property] = value 修改,这是普通写法,但有一些坑(比如继承场景、严格模式下可能出错)。
ES6 提供了 Reflect,它的作用就是:用 "更标准、更安全" 的方式,执行对象的基本操作(读取、修改等),和 Proxy 是 "最佳搭档" 。
Reflect 的核心用法(和 Proxy 对应)
Reflect 是一个内置对象,不能实例化,直接调用它的静态方法即可,最常用的就是 Reflect.get 和 Reflect.set,对应对象的 "读取" 和 "修改":
| 普通对象操作 | Reflect 对应操作 | 作用 |
|---|---|---|
| target[property] | Reflect.get(target, property) | 读取对象的某个属性值 |
| target[property] = value | Reflect.set(target, property, value) | 修改对象的某个属性值 |
js
const person = { name: "张三", age: 20 };
const proxyPerson = new Proxy(person, {
get(target, property) {
console.log(`有人读取了 ${property} 属性`);
// 用 Reflect.get 代替 target[property]
return Reflect.get(target, property);
},
set(target, property, value) {
console.log(`有人修改了 ${property} 属性,新值是 ${value}`);
// 用 Reflect.set 代替 target[property] = value
Reflect.set(target, property, value);
return true;
}
});
三、分步实现:基于 Proxy 的完整响应式
我们分 2 步实现,从简单到复杂,每一步都能独立运行,方便你测试理解。
步骤 1:封装一个基础的响应式函数(监听读写)
我们先把 Proxy 的逻辑封装成一个函数,方便复用(这就是 Vue3 中 reactive 函数的简化版)。
js
/**
* 封装响应式函数:给普通对象返回一个代理对象
* @param {Object} obj - 要被代理的原始对象
* @returns {Proxy} - 代理后的响应式对象
*/
function makeReactive(obj) {
// 创建 Proxy 代理
const proxyObj = new Proxy(obj, {
// 拦截读取操作
get(target, property) {
// 读取属性并返回
return Reflect.get(target, property);
},
// 拦截修改操作
set(target, property, value) {
// 执行修改操作
Reflect.set(target, property, value);
return true;
}
});
// 返回代理对象
return proxyObj;
}
步骤 2:升级:实现 "数据变 → 页面自动更新"
大家最关心的是:如何让数据变化时,页面 DOM 自动更新?我们基于上面的代码,新增 DOM 更新逻辑,更贴近实际项目使用。
完整代码(可直接复制到 HTML 文件中运行)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>新手版 Proxy 响应式</title>
<style>
.user-info {
margin: 20px;
padding: 20px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<!-- 页面DOM:要自动更新的内容 -->
<div class="user-info">
<p>用户名:<span id="username"></span></p>
<p>等级:<span id="level"></span></p>
</div>
<button id="upgradeBtn">点击升级等级</button>
<script>
// 步骤1:封装响应式函数(新增:DOM更新逻辑)
function makeReactive(obj) {
const proxyObj = new Proxy(obj, {
get(target, property) {
return Reflect.get(target, property);
},
set(target, property, value) {
// 1. 先修改数据
const oldValue = target[property];
Reflect.set(target, property, value);
// 2. 数据修改后,自动更新DOM(核心:响应式的实际落地)
console.log(`数据 ${property} 变化,自动更新页面`);
updateDOM(property, value);
return true;
}
});
return proxyObj;
}
// 步骤2:封装 DOM 更新函数(根据数据变化,更新对应页面元素)
function updateDOM(property, value) {
if (property === "username") {
// 找到页面上的 username 元素,更新内容
document.getElementById("username").innerText = value;
}
if (property === "level") {
// 找到页面上的 level 元素,更新内容
document.getElementById("level").innerText = value;
}
}
// 步骤3:初始化数据和页面
// 1. 原始数据
const user = {
username: "前端新手",
level: 1
};
// 2. 生成响应式对象
const reactiveUser = makeReactive(user);
// 3. 初始化页面(第一次手动渲染,后续自动更新)
updateDOM("username", reactiveUser.username);
updateDOM("level", reactiveUser.level);
// 步骤4:绑定按钮点击事件(修改数据,测试自动更新)
document.getElementById("upgradeBtn").addEventListener("click", function() {
// 只需要修改响应式数据,页面会自动更新!
reactiveUser.level = reactiveUser.level + 1;
alert(`等级升级成功!当前等级:${reactiveUser.level}`);
});
</script>
</body>
</html>
运行效果

-
打开 HTML 文件,页面上会显示用户名和等级
-
点击 "点击升级等级" 按钮
-
你会发现:
- 弹窗提示等级升级
- 页面上的 "等级" 数字会自动从 1 变成 2,再点击变成 3...
- 我们没有手动写
document.getElementById("level").innerText = 新值,而是数据变化后自动触发,这就是完整的响应式效果!
四、核心知识点(总结)
-
数据响应式:数据变 → 页面 / 逻辑自动变,无需手动操作。
-
Proxy:给对象加 "拦截器",监听「读取(get)」和「修改(set)」操作,是实现响应式的核心。
-
Reflect :和 Proxy 搭档,更安全地操作对象属性(替代
target[property]和target[property] = value)。 -
核心流程:
- 用
makeReactive函数把普通对象变成 Proxy 代理对象 - 操作代理对象时,会触发 get/set 拦截器
- 在 set 拦截器中,数据修改后自动执行 DOM 更新(或其他逻辑)
- 用
-
关键注意点:必须操作「Proxy 代理对象」才会触发响应式,操作原始对象无效。
五、常见疑问解答
- 问:为什么不用直接修改对象,非要用 Proxy? 答:直接修改对象无法 "监听" 变化,比如
user.level = 2,我们不知道什么时候改的、改了什么值;而 Proxy 能精准捕获这些操作,进而触发后续逻辑。 - 问:这和 Vue3 的响应式有什么关系? 答:Vue3 的
reactive函数底层就是基于 Proxy 实现的,我们写的makeReactive就是reactive的简化版,Vue3 只是在这个基础上增加了 "依赖收集" 等更复杂的逻辑(下一篇文章可能就会讲述,大家可以关注新的栏目"Vue源码解析")。 - 问:Reflect 必须用吗?不用行不行? 答:简单场景下不用也可以,但 Reflect 能解决很多坑(比如继承、严格模式),而且是 ES6 推荐的 Proxy 搭档,新手直接养成使用习惯即可。
六、拓展
既然都讲到这里啦,那就浅谈vue2和vue3中关于有无Proxy的区别吧~
1. Vue2 的响应式:基于 Object.defineProperty
Vue2 是通过 Object.defineProperty 给对象的每个属性 单独设置 getter/setter,从而监听属性的读写。
但它有几个明显的缺陷:
- 只能监听已存在的属性 :如果给对象新增属性(比如
obj.newKey = 1),Vue2 监听不到,得用Vue.set手动触发响应。 - 无法监听数组的变化 :比如数组的
push/pop/splice等方法,Vue2 是通过 "重写数组原型方法" 的方式曲线救国,不够优雅。 - 必须遍历对象的所有属性:如果对象层级深,需要递归遍历,性能开销大。
2. Vue3 的响应式:基于 Proxy
Vue3 用 Proxy 代理整个对象,直接拦截对象的「所有属性操作」,完美解决了 Vue2 的缺陷:
- 能监听新增 / 删除属性 :比如
obj.newKey = 1会被Proxy的set拦截,自动响应。 - 能原生监听数组变化 :数组的
push/pop等操作会被Proxy捕获,不用再重写数组方法。 - 代理整个对象,无需递归遍历:
Proxy是代理整个对象,层级深的对象会在读取时懒代理,性能更优。
总结
Vue3 用 Proxy 替代 Vue2 的 Object.defineProperty,本质是用更现代、更强大的 ES6 特性,解决了 Vue2 响应式的各种 "痛点" ~