一、Object.defineProperty
基本用法
Object.defineProperty(obj, prop, descriptor) 可以精确地定义或修改对象上的属性,其中 get 和 set 是拦截数据读取与写入的关键。
javascript
let obj = {};
let internalValue = 0;
Object.defineProperty(obj, 'count', {
get() {
console.log('读取 count');
return internalValue;
},
set(val) {
console.log('设置 count:', val);
internalValue = val;
}
});
obj.count; // 触发 get
obj.count = 5; // 触发 set
实现响应式的核心思路
利用 get 收集依赖(谁用到了这个属性),set 触发更新(通知所有依赖重新执行)。
致命缺陷
- 无法监听对象新增/删除属性
defineProperty只能劫持已存在的属性,新增obj.newProp = 1无法被感知,必须使用Vue.set/delete这种补丁手段。 - 无法监听数组索引和 length 变化
通过下标修改数组(arr[0] = 1)或修改length都无法触发 setter。 - 必须深度遍历 + 递归代理
在初始化时需要对 data 对象的所有层次的所有属性递归设置 getter/setter,遇到深层或大型对象会严重影响启动性能。 - 无法原生支持 Map、Set、WeakMap、WeakSet
二、ES6 Proxy
基本用法
new Proxy(target, handler) 创建一个对象的代理,handler 中可以定义多达 13 种拦截操作 ,包括 get、set、has、deleteProperty、ownKeys 等。
javascript
const target = { count: 0 };
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log(`读取 ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`设置 ${key} = ${value}`);
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
console.log(`删除 ${key}`);
return Reflect.deleteProperty(target, key);
}
});
proxy.count; // 读取 count
proxy.newProp = 10; // 设置 newProp = 10
delete proxy.newProp; // 删除 newProp
相较 defineProperty 的绝对优势
| 能力 | defineProperty | Proxy |
|---|---|---|
| 监听新增/删除属性 | ❌ 需要 Vue.set | ✅ 可拦截 set / deleteProperty |
| 监听数组下标和 length | ❌ | ✅ |
| 监听 Map、Set、WeakMap | ❌ | ✅ |
| 惰性代理(用到时才递归) | ❌ 必须深度遍历 | ✅ 仅在 get 时递归代理 |
| 劫持整个对象 | 属性级别 | 对象级别,更加灵活 |
| 性能 | 初始化时递归遍历全部属性 | 惰性递归,初始化极快 |
三、Vue2 响应式原理深度解析
Vue2 的响应式系统完全是建立在 Object.defineProperty 之上的,核心流程如下:
1. Observer(观察者)------ 将数据变为响应式
javascript
class Observer {
constructor(value) {
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
}
function defineReactive(obj, key, val) {
const dep = new Dep(); // 每个属性一个依赖管理器
// 递归处理嵌套对象
if (typeof val === 'object' && val !== null) {
new Observer(val);
}
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend(); // 收集当前 Watcher
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 新值如果是对象也要代理
if (typeof newVal === 'object' && newVal !== null) {
new Observer(newVal);
}
dep.notify(); // 通知所有 Watcher 更新
}
});
}
2. Dep(依赖管理器)
javascript
class Dep {
constructor() {
this.subs = []; // 存储 Watcher
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
3. Watcher(观察者)
Watcher 代表一个依赖,比如组件的渲染函数、计算属性、watch 回调等。
javascript
class Watcher {
constructor(getter, callback) {
this.getter = getter;
this.callback = callback;
this.get(); // 初始化时立刻求值,触发依赖收集
}
get() {
Dep.target = this;
this.value = this.getter(); // 执行 getter,触发数据属性的 getter,收集依赖
Dep.target = null;
return this.value;
}
update() {
this.run();
}
run() {
const oldValue = this.value;
this.value = this.get();
this.callback(this.value, oldValue);
}
}
4. 数组的特殊处理
由于 defineProperty 无法监听数组索引/长度变化,Vue2 选择重写数组的变异方法 (push, pop, shift, unshift, splice, sort, reverse),在保持原有功能的同时手动触发通知。
javascript
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value(...args) {
const result = original.apply(this, args);
const ob = this.__ob__; // Observer 实例
ob.dep.notify(); // 手动通知
return result;
}
});
});
// 在 Observer 中将数组的 __proto__ 指向 arrayMethods
5. Vue.set / Vue.delete
为解决新增/删除属性的问题,Vue2 额外提供了 Vue.set(obj, key, value),内部会调用 defineReactive 并通知更新。
四、Vue3 响应式原理深度解析
Vue3 全面拥抱 Proxy,并基于 Reflect 实现,其响应式系统更加简洁、强大。
1. reactive ------ 核心入口
javascript
function reactive(target) {
if (typeof target !== 'object' || target === null) return target;
// 防止重复代理
if (target.__v_isReactive) return target;
const proxy = new Proxy(target, baseHandlers);
proxy.__v_isReactive = true;
return proxy;
}
2. baseHandlers(以 get/set 为例)
javascript
const baseHandlers = {
get(target, key, receiver) {
// 如果是获取 raw 原始对象(如 toRaw),则返回 target
if (key === '__v_raw') return target;
const result = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 惰性递归:如果返回值是对象,继续用 reactive 包裹
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// 触发更新
trigger(target, key, value, oldValue);
return result;
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey) {
trigger(target, key, undefined, 'delete');
}
return result;
}
// 还有 has, ownKeys 等拦截,也会触发 track/trigger
};
3. track 和 trigger ------ 依赖收集与触发
Vue3 使用 WeakMap<target, Map<key, Set<effect>>> 存储依赖关系,完全解耦了 Dep 类。
javascript
const targetMap = new WeakMap();
let activeEffect = null; // 类似 Dep.target
function track(target, key) {
if (!activeEffect) 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(activeEffect);
}
function trigger(target, key, value, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect.run());
}
}
4. effect ------ 代替 Watcher
javascript
class ReactiveEffect {
constructor(fn) {
this.fn = fn;
}
run() {
activeEffect = this;
const result = this.fn(); // 执行函数,触发 getter 中的 track
activeEffect = null;
return result;
}
}
function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run(); // 立即执行一次,收集依赖
return _effect;
}
5. ref 的实现
针对基本类型,Vue3 使用一个包含 .value 的对象,内部仍是依赖追踪。
javascript
function ref(value) {
const obj = {
get value() {
track(obj, 'value');
return value;
},
set value(newValue) {
if (newValue !== value) {
value = newValue;
trigger(obj, 'value');
}
}
};
return obj;
}
// 或直接使用 reactive 包裹 { value }
6. computed 和 watch
computed 本质是一个惰性 effect,内部有 _dirty 标记实现缓存;watch 则是在 effect 基础上监听特定响应式对象的变化。
五、Vue2 vs Vue3 响应式核心差异对比
| 维度 | Vue2 | Vue3 |
|---|---|---|
| 数据劫持方式 | Object.defineProperty(属性级别) | Proxy(对象级别) |
| 新增/删除属性 | 无法自动监听,需要 Vue.set/delete | 原生支持(deleteProperty / set) |
| 数组监听 | 重写数组方法,下标/长度无法监听 | 完美支持下标、length、及所有方法 |
| Map/Set/WeakMap | 不支持 | 原生支持 |
| 初始化性能 | 深度递归遍历所有属性,一次性开销大 | 惰性递归(get 时才代理),初始化极快 |
| 内存开销 | 每个属性生成 getter/setter + Dep,深层对象内存大 | 每个对象一个 Proxy,依赖关系存于 WeakMap,内存更可控 |
| API 设计 | 基于组件选项的 data、computed、watch,耦合度高 | 独立响应式 API(reactive, ref, computed 等),可脱离组件使用 |
| Reflect 使用 | 无 | 全面使用 Reflect,确保 this 正确和返回值布尔 |
六、总结
- Object.defineProperty 凭借其精准的属性劫持能力成为 Vue2 的基石,但因设计上的硬伤(数组、属性增删),不得不借助补丁手段,导致响应式系统存在诸多限制。
- Proxy 则是 ES6 带来的革命性元编程能力,能够劫持一个对象的几乎全部操作,使得响应式系统变得完整、自然、高效 。Vue3 的全新响应式架构正是建立于此,配合
track/trigger和effect,不仅彻底解决了 Vue2 遗留的难题,还将响应式逻辑从组件中抽离出来,实现了 Composition API 的灵活与强大。
理解这两种响应式核心机制,不仅能加深对 Vue 框架的认知,更是掌握现代前端数据驱动思想的必经之路。