能背出「Vue3 用 Proxy 代替 defineProperty」只值 0 分。讲清 Vue2 哪些场景天生处理不了、Proxy 又漏了哪些坑、Vue3 为什么留着 markRaw,才算及格。
一句正确的废话,值 0 分
「Vue2 和 Vue3 的响应式有什么区别?」
候选人张口就来:Vue2 用 Object.defineProperty,Vue3 用 Proxy。
没错,但这是句废话。好比问 MySQL 和 PostgreSQL 的区别,你答「一个用 B+ 树一个也用 B+ 树」------你报的是 API 名字,不是工程判断。
真正该往下追问三层:defineProperty 的硬伤是什么?Proxy 补了哪些线上坑?换完之后,还有哪些场景 Proxy 也绕不开?
defineProperty 的签名就写死了它的上限
Vue2 通过 Object.defineProperty 把 data 上的每个属性转成 getter/setter,读取时收集依赖,赋值时触发更新。
js
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.depend() // 收集当前 Watcher
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify() // 通知更新
}
})
}
关键限制藏在 defineReactive 的签名里:它一次只能处理一个 key,而且是调用那一刻已经存在的 key。
所以 obj.newField = 1 和 delete obj.field 这两个操作,defineProperty 根本没机会介入。
四个硬伤,两个是数组,两个是对象
三年经验的人在这一层开始模糊。具体是四条:
- 数组下标赋值监听不到 :
arr[0] = 'x'不会触发 setter,因为 Vue2 压根没给数组下标做 defineProperty - 数组长度修改监听不到 :
arr.length = 0同理 - 对象新增、删除属性监听不到 :所以才有
Vue.set / this.$$set和Vue.delete - 只支持普通对象和数组:Map、Set、WeakMap 这类原生集合类型完全无能为力
前两条的补丁是重写七个数组原型方法:
js
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
methodsToPatch.forEach(method => {
const original = Array.prototype[method]
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
if (method === 'push' || method === 'unshift') inserted = args
else if (method === 'splice') inserted = args.slice(2)
if (inserted) ob.observeArray(inserted) // 新插入的元素也要变成响应式
ob.dep.notify()
return result
})
})
很多人知道要写 this.$$set,但说不出为什么非它不可------因为 defineProperty 的拦截能力在定义那一刻就封死了。
来看 Vue2 的完整数据流,注意那条断掉的支路:
Proxy 填了旧坑,自己又挖了新坑
Proxy 代理的是整个对象,不是某个 key,所以天生能拦截 13 种操作。
js
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver)
return isObject(res) ? reactive(res) : res // 懒代理,访问到才递归
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) trigger(target, key) // 新增 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
}
})
}
对比一下两边到底谁修好了谁:
| 场景 | Vue2 defineProperty | Vue3 Proxy |
|---|---|---|
| 新增属性 | 不触发,要 $$set | 原生触发 |
| 删除属性 | 不触发,要 $$delete | 原生触发 |
| 数组下标赋值 | 不触发 | 原生触发 |
| 数组长度修改 | 不触发 | 原生触发 |
| Map / Set | 不支持 | 支持 |
| 初始化开销 | 递归遍历所有 key | 懒代理,访问才递归 |
| IE11 兼容 | 支持 | 不支持 |
Reflect 存在的意义不是仪式感,是保证 receiver 正确传递 ,让 this 在继承和代理链里指向代理对象而不是原始对象。少了它,super 访问和 getter 里的 this 都会指错。
那 Proxy 就没坑了?想得美,至少三类:
js
const state = reactive({ count: 0, nested: { a: 1 } })
// 1. 解构:拿到的是普通 number,跟响应式断联
let { count } = state
count++ // 视图不动
// 2. 展开:copy 是全新的普通对象
const copy = { ...state }
// 3. 直接改原始对象:Proxy 是包装,不是克隆
const raw = { a: 1 }
const p = reactive(raw)
raw.a = 2 // 绕过代理,不触发更新
还有 ref 传原始值、reactive 不接受基础类型------因为 Proxy 的 target 必须是对象,Proxy(1, {}) 直接抛 TypeError 。这就是 Ref 存在的全部理由:给原始值套一层 { value } 的壳,再对这个壳做代理。
markRaw 不是摆设,是性能账
到这一层才叫懂框架。
为什么 Vue3 还留着 markRaw? 因为不是所有数据都值得被代理。一个 5000 行的 ECharts options、一个 Frozen 的配置常量、一个第三方库的实例对象------把它们塞进 reactive,Proxy 的 get/set 拦截会在每次访问时叠加开销。
js
import { markRaw } from 'vue'
const state = reactive({
chart: markRaw(echarts.init(dom)) // 打标记,跳过代理
})
markRaw 的实现就是加个标志位:
js
export function markRaw(value) {
def(value, ReactiveFlags.SKIP, true)
return value
}
reactive 内部一看到 SKIP 就直接返回原对象。这也是为什么 shallowRef / shallowReactive 在大数据列表场景是刚需------几万条数据的列表,全量递归代理的内存开销和 GC 压力都不是小数目。
性能账也要算清楚:
- 初始化:Vue2 必须递归遍历 data 的所有 key 打 defineProperty,对象越大启动越慢;Vue3 是懒代理,访问到哪层代理哪层
- 更新:Vue2 每次 set 直接打 dep;Vue3 要多走一层 Proxy 的 trap + Reflect,单次访问略慢,但省掉了递归初始化的巨坑
- 兼容性 :Proxy 是浏览器原生 API,IE 完全不支持,且无法 polyfill------这是 Vue3 彻底放弃 IE11 的核心原因,不是团队懒,是能力边界
下次这么答
再被问到这道题,别急着报 API 名字。按这个顺序说:defineProperty 只能拦截已有 key,所以数组下标、对象增删、Map/Set 全都漏;Proxy 代理整个对象把这些补上,代价是原始值的 Ref 包装、解构和展开的断联、以及 IE 的彻底出局。
面试官想听的不是「Proxy 比 defineProperty 好」,而是「Vue3 在能力、性能、兼容性之间怎么做的取舍」。
写在最后
你们线上被响应式坑过吗?我见过最典型的是 reactive 里放了一个第三方实例,结果每次改它的内部属性都触发视图更新,白烧性能。评论区聊聊你们踩过最刁钻的 Vue 响应式坑。
有用的话点个赞。