用过 Vue2 好些年,你真的懂它为什么需要$set吗?Vue3 又为什么换成Proxy?这篇不用官方术语堆砌,就用我当年真实踩过的坑,把这俩的区别掰开揉碎讲清楚。
先说个丢人的事
前阵子面试,对方随口问了句:"为什么 Vue3 用 Proxy,不用 Vue2 那套Object.defineProperty了?"
我当场卡了大概半分钟。Vue2 我用了五年,Vue3 也上手做项目了,日常$set用得贼溜——但真被问到"为什么"的时候,发现自己只是会用,根本没真懂。
回来翻了翻源码和文档,才把这块理顺。今天借这个机会,把自己的理解记下来,也算是给当年那个卡壳的自己一个交代。
用包租婆查水电费,理解 Vue2 的响应式
先把 Vue2 的响应式想成包租婆管出租屋:
租出去每一间房,包租婆都单独装了一块电表。哪间房用电了,对应那块表就转,包租婆就知道"这间房状态变了,去收电费"。
但问题来了——你要是新租出去一间房,却忘了装电表,这间房用再多电,包租婆也收不到信号。
Vue2 里这个"装电表"的动作,就是Object.defineProperty给每个属性加getter/setter。它在组件初始化时,把data里已有的每个属性都"装好表"。所以:
- 你一开始写在
data里的字段,改了能触发更新,没问题; - 但你运行时新加的属性(比如后台返回的对象没带某个字段,你直接
obj.newKey = 1),这块"新房间"没装表,改了也没人知道,视图不更新。
这就是当年$set存在的根本原因——它是用来给"新房间"补装电表的。
我当年被坑的两个瞬间
第一个:this.user.age = 18不更新。
做中后台那会儿,接口返回的用户对象没带age字段。我在mounted里直接this.user.age = 18,页面死活不动。查了半天才反应过来:这个age是运行时加的,Vue2 管不到。得用this.$set(this.user, 'age', 18)才行。
后来我学乖了,凡是"可能后端不给全"的对象,先Object.assign一个带默认值的模板,提前把字段都声明好,把坑填在前面。
第二个:数组下标赋值不生效。
有一次this.list[0] = newValue,视图没反应。我甚至一度怀疑是框架 bug(现在想想真丢人)。最后发现 Vue2 根本拦不住数组的下标赋值,得用splice或者$set。
这俩坑看着像"我代码写错了",其实根子上是同一个——defineProperty的能力边界。
Vue2 这套方案,到底有哪几个硬伤
踩完坑再回头看,就通透了。Object.defineProperty有三个绕不开的问题:
- 必须预先遍历已知属性。新增、删除属性它收不到信号(就是上面"新房间没装表")。
- 数组拦截不到。
push/pop这些方法 Vue2 是重写的(算 hack),但下标赋值这个原生操作它拦不住。 - 深层对象要递归遍历。嵌套越深,初始化时递归成本越高,而且是一上来就全量铺开的。
Vue3 怎么把这些问题端了的
Vue3 换成了Proxy,等于把"每间房装电表"改成了整栋楼装一个中央总闸:
不管你是新住户、还是把房间拆了重建(新增/删除属性、数组下标改值),只要有人进出,总闸统一拦截、统一通知。所以 Vue3 里普通赋值就行,没有$set这层心智负担。
Proxy代理的是整个对象,get/set/deleteProperty/has全都能拦,而且是惰性的——你访问到哪一层,它才代理哪一层,不用一开始全量遍历。正好把上面三个硬伤一一化解。
两段代码,对比着看更直观
js
// Vue2:给每个属性单独"装电表" function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { dep.depend() // 收集依赖 return val }, set(newVal) { val = newVal dep.notify() // 通知更新 } }) }js
// Vue3:整栋楼交给一个 Proxy 总闸 const reactive = (obj) => new Proxy(obj, { get(target, key, receiver) { track(target, key) // 收集依赖 const res = Reflect.get(target, key, receiver) return typeof res === 'object' ? reactive(res) : res // 惰性代理 }, set(target, key, value, receiver) { Reflect.set(target, key, value, receiver) trigger(target, key) // 通知更新 return true } })一句话总结差异:Vue2 是逐属性上锁,Vue3 是整栋楼门禁。
顺带提一句 React——它那边不玩"拦截对象"这套,是让你返回新状态(setState/ 不可变更新)来触发重渲染,完全是另一条路子,这里就不展开了。
给后来者的几句大实话
我熬了几年才明白:会用$set不等于懂响应式。真正懂,是能回答"它为什么存在""换 Proxy 解决了什么"。
所以如果你也在用 Vue,别像我一样只停在"能跑就行"。花一个下午把defineProperty和Proxy这两段最朴素的实现读一遍,比你刷十个面试题都值。
你们当年有没有被 Vue2 的响应式坑过?评论区聊聊,让我知道自己不是一个人在战斗。