中国蓝tv手机版
143.87MB · 2025-10-13
标签: Proxy、defineProperty、原生 JS、响应式、Vue3、性能
Vue3 都发布 4 年了,「Proxy 取代 defineProperty」早成旧闻。
但面试里总有人被追问:
今天就用纯浏览器可跑的代码回答这个问题,最后 5 行顺带告诉你 Vue3 为什么笑出声。
Vue.set
/ vm.$delete
。push/pop
等 7 个方法。下面所有代码你都可以直接粘到 Chrome 控制台玩。
// defineProperty 版本
function observeObj(obj) {
for (const key in obj) {
let internal = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log(`[defineProperty] get ${key}`);
return internal;
},
set(newVal) {
console.log(`[defineProperty] set ${key} = ${newVal}`);
internal = newVal;
}
});
}
}
const o = { a: 1 };
observeObj(o);
o.a++; // 有日志
o.b = 2; // 监听不到
delete o.a; // 监听不到
// Proxy 版本
const handler = {
get(target, key, receiver) {
console.log(`[Proxy] get ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, val, receiver) {
console.log(`[Proxy] set ${key} = ${val}`);
return Reflect.set(target, key, val, receiver);
},
deleteProperty(target, key) {
console.log(`[Proxy] delete ${key}`);
return Reflect.deleteProperty(target, key);
}
};
const p = new Proxy({ a: 1 }, handler);
p.a++; // 有日志
p.b = 2; // 一样有日志
delete p.a;// 还是日志
结论:
Proxy 一次性代理整对象,13 种 trap 想拦谁就拦谁;
defineProperty 只能给已有属性挨个装门禁。
// defineProperty 对数组束手无策
const arr = [1, 2, 3];
observeObj(arr); // 只会监听 0/1/2 索引
arr.push(4); // 无日志,length 也不变
// Proxy 直接无痛
const arrP = new Proxy(arr, handler);
arrP.push(4); // 日志:[Proxy] set 3 = 4 、[Proxy] set length = 4
MacBook Air M1 / Chrome 119 / 10 万次操作
场景 | defineProperty | Proxy | 差距 |
---|---|---|---|
新增 1 万属性 | 580 ms | 42 ms | 13× |
数组 push 1 万次 | 320 ms | 28 ms | 11× |
// 全局副作用栈
const effectStack = [];
function effect(fn) {
const wrapped = () => {
effectStack.push(wrapped);
fn();
effectStack.pop();
};
wrapped();
}
const targetMap = new WeakMap(); // { target: Map{ key: Set<effect> } }
function track(target, key) {
const effect = effectStack[effectStack.length - 1];
if (!effect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
dep.add(effect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
depsMap?.get(key)?.forEach(fn => fn());
}
function reactive(obj) {
return new Proxy(obj, {
get(t, k, r) { track(t, k); return Reflect.get(t, k, r); },
set(t, k, v, r) { const res = Reflect.set(t, k, v, r); trigger(t, k); return res; }
});
}
/* ====== 使用 ====== */
const state = reactive({ count: 0 });
effect(() => { document.body.innerText = state.count; });
setInterval(() => state.count++, 1000);
把上面 40 行粘进空白 index.html
,双击打开,整个页面每秒自动刷新数字——零依赖。
state.list[3] = x
或 delete state.obj.a
,无需 set/$delete
;<script setup>
编译期直接缓存 Proxy
引用,跳过运行时 toReactive
判断,内存降 20%;defineProperty
兼容代码整体砍掉 12 KB(gzip)。get
陷阱有不可优化的隐形成本。