Vue 2 用 Object.defineProperty 实现响应式,但无法监听新增/删除属性和数组索引赋值;Proxy 可全面拦截且支持懒代理,但不兼容 IE;v-model 是语法糖,依赖底层响应式系统;脏检查和发布-订阅是兼容性好但有性能或内存泄漏风险的替代方案。

Object.defineProperty 是最直接的劫持方式
在 Vue 2 中,Object.defineProperty 是实现响应式的核心。它能监听对象属性的 get 和 set,从而在读取时收集依赖、赋值时触发更新。
关键限制在于:它无法监听新增/删除属性,也不能直接代理数组索引赋值(如 arr[0] = 1),所以 Vue 2 对数组方法做了重写,对对象则要求用 Vue.set。
- 必须遍历对象所有已有属性调用
Object.defineProperty,否则后续添加的属性不响应 - 嵌套对象需递归处理,否则深层属性修改不会触发视图更新
- 不能代理 Map、Set、class 实例等非普通对象
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
let internalValue = obj[key];
observe(internalValue); // 递归
Object.defineProperty(obj, key, {
get() { console.log('get', key); return internalValue; },
set(newVal) {
console.log('set', key, newVal);
internalValue = newVal;
// 这里应通知 watcher 更新
}
});
});
}
Proxy 可以替代 defineProperty 实现更完整的拦截
Proxy 是 ES6 提供的原生代理机制,能拦截对象的任意操作:读取、赋值、in、delete、has、iterate,甚至数组索引和长度变更。
相比 Object.defineProperty,它天然支持动态增删属性、数组下标赋值、Map/Set,且无需递归初始化——可以懒代理(访问时再代理子属性)。
立即学习“Java免费学习笔记(深入)”;
- 一个
Proxy实例只能代理一层,仍需在get中对返回值做代理(即“懒代理”) - 不能直接代理普通函数或原始值,需包装成对象
- IE 完全不支持,若需兼容低版本浏览器,不能单独使用
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
return new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 这里可做依赖收集
return typeof res === 'object' && res !== null ? reactive(res) : res;
},
set(target, key, newVal, receiver) {
const oldVal = target[key];
const res = Reflect.set(target, key, newVal, receiver);
// 这里可触发更新
return res;
}
});
}
v-model 的本质是语法糖,不是双向绑定的实现机制
v-model 在 Vue 中只是对 :value + @input(或特定事件)的封装;React 的 value + onChange 组合也同理。它们本身不提供响应式能力,只是约定好的“受控组件”写法。
真正让数据变化驱动 UI、UI 输入又反向更新数据的,是底层响应式系统(defineProperty 或 Proxy)+ 视图更新调度(如 queueFlush)共同完成的。
-
v-model在不同元素上会解析为不同事件:input、change、update:modelValue - 自定义组件要支持
v-model,需显式声明modelValueprop 并触发update:modelValue事件 - 没有响应式系统支撑,
v-model只是单向绑定的简写,无法自动同步
脏检查和发布-订阅是绕过语言限制的替代思路
AngularJS 用的是脏检查($digest 循环),通过定时比对新旧值来判断是否更新;而 RxJS 或手写 EventEmitter 则属于典型的发布-订阅模式:数据变更时主动 emit,视图 subscribe 后响应。
这两种方式不依赖语言特性,因此兼容性极好,但代价明显:脏检查有性能开销,尤其在大量 watcher 场景;发布-订阅需要手动调用 emit,容易遗漏或重复触发。
- 脏检查无法感知异步任务外的变更(比如
setTimeout外部改值),需手动$apply - 发布-订阅中,如果忘记取消订阅(
unsubscribe),容易造成内存泄漏 - 两者都无法自动追踪嵌套属性路径(如
a.b.c),需靠路径字符串或 proxy 包装兜底
Proxy 是当前最平衡的选择,但要注意它的代理不可逆、无法被 JSON.stringify 正常序列化,且调试时控制台显示为 Proxy 对象而非原始结构——这些细节往往在联调或 SSR 场景中才突然暴露。











