Vue 2 用 Object.defineProperty 实现响应式,但无法监听新增/删除属性和数组索引赋值;Proxy 可全面拦截且支持懒代理,但不兼容 IE;v-model 是语法糖,依赖底层响应式系统;脏检查和发布-订阅是兼容性好但有性能或内存泄漏风险的替代方案。
在 Vue 2 中,Object.defineProperty 是实现响应式的核心。它能监听对象属性的 get 和 set,从而在读取时收集依赖、赋值时触发更新。
关键限制在于:它无法监听新增/删除属性,也不能直接代理数组索引赋值(如 arr[0] = 1),所以 Vue 2 对数组方法做了重写,对对象则要求用 Vue.set。
Object.defineProperty,否则后续添加的属性不响应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 是 ES6 提供的原生代理机制,能拦截对象的任意操作:读取、赋值、in、delete、has、iterate,甚至数组索引和长度变更。
相比 Object.defineProperty,它天然支持动态增删属性、数组下标赋值、Map/Set,且无需递归初始化——可以懒代理(访问时再代理子属性)。
Proxy 实例只能代理一层,仍需在 get 中对返回值做代理(即“懒代理”)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 在 Vue 中只是对 :value + @input(或特定事件)的封装;React 的 value + onChange 组合也同理。它们本身不提供响应式能力,只是约定好的“受控组件”写法。
真正让数据变化驱动 UI、UI 输入又反向更新数据的,是底层响应式系统(defineProperty 或 Proxy)+ 视图更新调度(如 queueFlush)共同完成的。
v-model 在不同元素上会解析为不同事件:input、change、update:modelValue
v-model,需显式声明 modelValue prop 并触发 update:modelValue 事件v-model 只是单向绑定的简写,无法自动同步AngularJS 用的是脏检查($digest 循环),通过定时比对新旧值来判断是否更新;而 RxJS 或手写 EventEmitter 则属于典型的发布-订阅模式:数据变更时主动 emit,视图 subscribe 后响应。
这两种方式不依赖语言特性,因此兼容性极好,但代价明显:脏检查有性能开销,尤其在大量 watcher 场景;发布-订阅需要手动调用 emit,容易遗漏或重复触发。
setTimeout 外部改值),需手动 $apply
unsubscribe),容易造成内存泄漏a.b.c),需靠路径字符串或 proxy 包装兜底Proxy 是当前最平衡的选择,但要注意它的代理不可逆、无法被 JSON.stringify 正常序列化,且调试时控制台显示为 Proxy 对象而非原始结构——这些细节往往在联调或 SSR 场景中才突然暴露。