深入浅出Vue 响应式原理:从Object.defineProperty 到 Proxy

我有个技术全面、经验丰富的小团队,可承接各类网站、跨端、小程序等项目,有需要可私信我

大家好, 今天我们来聊一聊 Vue 框架中最核心的部分——响应式系统。

引言:为什么响应式是 Vue 的灵魂?

响应式是Vue的灵魂,因为它实现了Vue最核心的承诺:数据驱动视图
这不仅仅是技术实现,更是根本性的设计哲学,直观的数据、视图绑定,虚拟的DOM实现,框架内所有API的支撑等。
响应式让Vue从"一个UI库"变成了"一套思维范式"。它决定了你如何思考问题(关注数据状态而非DOM操作),而不仅仅是如何写代码。

一、响应式系统演进史

1.1 初代响应式:脏值检测(AngularJS)

在 Vue 出现之前,AngularJS 使用脏值检测机制。问题:性能开销大,需要遍历所有监视器。

// AngularJS 的脏检查
scope.$watch('user.name', function(newVal, oldVal) {
	console.log('用户姓名变化:', oldVal, '->', newVal);
});
// 需要手动触发检测
scope.$digest();

1.2 Vue1 的细粒度响应式

Vue1 为每个数据创建 Watcher,实现细粒度更新。虽然能精准更新,但是Watcher 过多,内存消耗大。

// Vue1 响应式理念
new Vue({
	data: {
		user: { name: '张三' }
	},
	watch: {
		'user.name': function(val) {
			console.log('姓名变化:', val);
		}
	}
});

1.3 Vue2 的改进:依赖收集与批量更新

Vue2 引入了虚拟 DOM 和组件级响应式

// Vue2 组件级更新
export default {
	data() {
		return {
			user: { name: '张三' },
			posts: []
		};
	},
	methods: {
		updateUserName() {
			this.user.name = '李四'; // 触发组件重新渲染
		}
	}
};

二、Vue2 Object.defineProperty 深度解析

2.1 Object.defineProperty

Object.defineProperty 是 ES5 提供的对象属性定义工具,它的核心能力是劫持属性的访问和赋值操作。就像给数据安装了一扇门,每次读写都会被记录和控制。

普通对象/数组   →   观测器(Observer)   →   属性劫持   →   响应式变量
      ↓              ↓                      ↓            ↓
{name: '张三'}     递归遍历每个属性      getter/setter    数据变化自动更新

defineProperty 的核心语法

Object.defineProperty(obj, 'property', {
	configurable: true,    // ① 能否删除属性或重新定义
	enumerable: true,      // ② 能否被 for-in 循环枚举
	writable: true,        // ③ 能否被赋值修改
	value: undefined,      // 直接设置值(与 get/set 互斥)
	// 核心拦截器👇
  get() { ... },         // 劫持"读"操作
  set(newVal) { ... }    // 劫持"写"操作
})

下面**手写一个简单的响应式系统(Observe函数)**来理解原理。

function defineReactive(obj, key, val) {
	// 每个属性都创建一个"依赖收集器"
	const deps = [];
	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get() {
			// 收集依赖:当前有"观察者"在读取这个属性吗?
			if (currentWatcher) {
				// 记录:这个观察者依赖我
				deps.push(currentWatcher);
			}
			return val;
		},
		set(newVal) {
			if (val === newVal) return;
			val = newVal;
			// 更新:我的值变了,通知所有依赖我的观察者
			deps.forEach(watcher => {
				watcher.update(); // 触发更新
			});
		}
	});
}
// 遍历对象使其响应式
function observe(obj) {
	if (typeof obj !== 'object' || obj === null) {
		return;
	}
	Object.keys(obj).forEach(key => {
		defineReactive(obj, key, obj[key]);
	});
}
// 使用
const data = { user: '张三' };
observe(data);
data.user = '李四'; // 触发更新

2.2 存在的局限性

1)数组操作的限制
通过索引设置数值vm.items[0] = '新值' ,修改数组长度vm.items.length = 0 都不会触发更新,因为并没有对数组的索引、数组的length属性,使用Object.defineProperty进行劫持,因为这样做的性能开销很大。

// 对应解决方案
Vue.set(vm.items, 0, '新值');
// 或
vm.items.splice(0, 1, '新值');

2)动态增删属性
对象中的属性增加vm.user.newProp = 'value'、删除delete vm.user.name 不会触发更新,因为新属性没有被Object.defineProperty处理,删除属性时vue也无法知道属性被删除了,也就无法通知依赖更新。
对应解决方案如下:

// 对于新增属性
Vue.set(vm.user, 'newProp', 'value');
// 或
vm.$set(vm.user, 'newProp', 'value');
// 对于删除属性
Vue.delete(vm.user, 'name');
// 或
vm.$delete(vm.user, 'name');

2.3 总结

Vue2响应式核心思想
· 递归劫持:深度遍历对象的所有属性,用 Object.defineProperty 重写 getter/setter
· 依赖收集:在 getter 中收集谁在"观察"这个数据
· 派发更新:在 setter 中通知所有观察者数据变化了
· 数组特例:重写数组方法来实现响应式
· 动态增删:通过 Vue.set/delete 特殊 API 处理

三、Vue3 Proxy:新响应式方案

3.1 Proxy代理

Proxy是 ES6 引入的一个强大的元编程特性,它允许你创建一个对象的代理,从而可以拦截和定义该对象的基本操作,就像给对象包装一个盒子,对象内数据的读写都要经过这个盒子的处理。
Proxy的基本语法const proxy = new Proxy(target, handler)
target:包装对象(任何类型的对象,包括数组、函数,甚至另一个代理)。
handler:通常为函数,函数内各属性中的实现分别定义了在执行各种操作时代理的行为。

Handler 对象可以定义以下拦截方法(部分):
get(target, property, receiver):拦截对象属性的读取。
set(target, property, value, receiver):拦截对象属性的设置。
deleteProperty(target, property):拦截 delete 操作。
defineProperty(target, property, descriptor):拦截 Object.defineProperty()setPrototypeOf(target, prototype):拦截 Object.setPrototypeOf()apply(target, thisArg, argumentsList):拦截函数的调用、call 和 apply 操作。
construct(target, argumentsList, newTarget):拦截 new 操作符。

Vue 3 的响应式系统使用 Proxy 来追踪对象属性的访问和修改当读取属性时,收集依赖;当修改属性时,触发更新。

下面手写一个基于Proxy的响应式来理解其原理。

// 使用 Proxy 实现响应式
function reactive(target) {
	// 返回一个代理对象
	return new Proxy(target, {
		// 拦截读方法
		get(target, key, receiver) {
			// Reflect 会保持对象的所有约束(如不可写属性、setter等)
			const res = Reflect.get(target, key, receiver);
			track(target, key); // 收集依赖
			// 如果结果是对象,则递归转为响应式
			return typeof res === 'object' ? reactive(res) : res;
		},
		// 拦截写方法
		set(target, key, value, receiver) {
			const oldValue = target[key];
			const result = Reflect.set(target, key, value, receiver);
			if (result && oldValue !== value) {
				trigger(target, key); // 触发更新
			}
			return result;
		},
		// 拦截删除属性方法
		deleteProperty(target, key) {
			const hadKey = hasOwn(target, key);
			const result = Reflect.deleteProperty(target, key);
			if (result && hadKey) {
				trigger(target, key); // 触发更新
			}
			return result;
		}
	});
}
// 依赖收集
const targetMap = new WeakMap();
function track(target, key) {
	if (activeEffect) {
		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) {
	const depsMap = targetMap.get(target);
	if (!depsMap) return;
	//
	const effects = depsMap.get(key);
	effects && effects.forEach(effect => effect());
}

3.2 存在的局限性

· Proxy 只能代理对象,不能代理基本类型(如字符串、数字、布尔值)。
· Proxy 的 this 问题:在 Proxy 的 handler 方法中,this 指向的是 handler 对象,而不是被代理的目标对象。因此,在需要访问目标对象时,通常使用第一个参数 target。

const obj = {
	name: 'Vue',
	getName() { return this.name } // this 指向谁?
}
const proxy = new Proxy(obj, {
	get(target, key, receiver) {
		// receiver 是 proxy 本身
		const value = Reflect.get(target, key, receiver);
		if (typeof value === 'function') {
			// 绑定正确的 this
			return value.bind(receiver)
		}
		return value
	}
})
console.log(proxy.getName())  // ✅ 正确输出 'Vue'

· 代理对象的原型链:Proxy 可以代理整个对象,包括原型链。但若目标对象是一个原型链上的对象,那么对原型链上属性的访问也会被拦截。

3.3 总结:Proxy 的时代意义

Proxy 代表了 **JavaScript 元编程的成熟。**它不仅仅是 Vue 3 响应式的实现基础,更是:
· 语言能力的体现:ES6 给开发者提供的"底层钩子"
· 设计模式的典范:代理模式的完美实现
· 未来框架的基础:为更多创新性框架提供可能
· 开发者思维的转变:从"如何修改对象"到"如何描述对象行为"

四、defineProperty VS Proxy 性能优化的全方位对比

4.1 内存优化:按需代理 vs 全量劫持

Object.defineProperty是一次性劫持所有属性;Proxy是按需代理。因此在内存优化方面,大型对象中Proxy 可以节省 30-50% 的内存。

//Object.defineProperty
function defineAllProperties(obj) {
	// 循环挟持多有属性
	Object.keys(obj).forEach(key => {
		defineReactive(obj, key, obj[key]);
	});
	// 对于嵌套对象
	Object.keys(obj).forEach(key => {
		if (typeof obj[key] === 'object') {
			defineAllProperties(obj[key]); // 递归劫持
		}
	});
}
// Proxy
const proxy = new Proxy(obj, {
	get(target, key) {
		const value = target[key];
		if (typeof value === 'object' && value !== null) {
			// 只有访问到时才代理嵌套对象
			return reactive(value);
		}
		return value;
	}
});

4.2 性能基准

编写测试代码来进行性能测试。

// 测试代码
const testData = { /* 包含1000个属性的对象 */ };
// Object.defineProperty 版本
console.time('defineProperty');
const observed1 = observeWithDefineProperty(testData);
console.timeEnd('defineProperty');
// Proxy 版本
console.time('proxy');
const observed2 = observeWithProxy(testData);
console.timeEnd('proxy');
// 访问性能测试 defineProperty
console.time('access-defineProperty');
for (let i = 0; i < 10000; i++) {
	observed1['prop' + (i % 1000)];
}
console.timeEnd('access-defineProperty');
// 访问性能测试 proxy
console.time('access-proxy');
for (let i = 0; i < 10000; i++) {
	observed2['prop' + (i % 1000)];
}
console.timeEnd('access-proxy');

初始化:Proxy 稍慢,但可接受
访问速度:两者相当
内存占用:Proxy 明显更低

五、Proxy 在实际项目中的应用

5.1 Vue3 响应式源码解析

下面是简化版的Vue3的响应式核心

function createReactiveObject(target, baseHandlers) {
	// isObject 是类型检查函数,判断是否为对象或数组
	if (!isObject(target)) {
		return target;
	}
	// 从 proxyMap 中查找是否已经有该对象的代理
	const existingProxy = proxyMap.get(target);
	if (existingProxy) {
		return existingProxy;
	}
	// 不存在的则创建新的 Proxy 代理对象
	const proxy = new Proxy(target, baseHandlers);
	proxyMap.set(target, proxy);
	return proxy;
}
// 基本处理器
const mutableHandlers = {
	get(target, key, receiver) {
		// 依赖收集
		track(target, key);
		const res = Reflect.get(target, key, receiver);
		// 自动解包 ref
		if (isRef(res)) {
			return res.value;
		}
		// 深层响应式
		// 只在访问对象属性时才递归转换为响应式, 惰性代理
		if (isObject(res)) {
			return reactive(res);
		}
		return res;
	},
	set(target, key, value, receiver) {
		const oldValue = target[key];
		// 处理 ref 赋值
		if (isRef(oldValue) && !isRef(value)) {
			oldValue.value = value;
			return true;
		}
		// 检查属性是否已经存在(用于区分新增和修改)
		// hasOwn 检查对象自身是否有该属性(不包括原型链)
		const hadKey = hasOwn(target, key);
		const result = Reflect.set(target, key, value, receiver);
		// 只有对象变化时才触发更新
		// 确保触发更新的是原始对象,而不是其他代理对象,toRaw 获取代理对象的原始对象, 这个检查避免重复触发更新
		if (target === toRaw(receiver)) {
			if (!hadKey) {
				trigger(target, key, TriggerOpTypes.ADD);
			} else if (hasChanged(value, oldValue)) {
				trigger(target, key, TriggerOpTypes.SET);
			}
		}
		return result;
	}
};

5.2 自定义响应式工具库

// 响应式状态管理器
class ReactiveStore {
	// 构造函数
	constructor(data = {}) {
		// 使用 reactive 函数将传入的数据转换为响应式数据
		this.data = reactive(data);
		// 初始化一个 Map 来存储监听器,key: 属性名;value: 该属性的监听函数集合(Map)
		this.listeners = new Map();
	}
	// 订阅特定字段
	subscribe(key, callback) {
		// 获取该属性已有的监听器集合,如果没有则创建新的 Set
		const listeners = this.listeners.get(key) || new Set();
		// 将新的回调函数添加到监听器集合中
		listeners.add(callback);
		// 将更新后的监听器集合保存回 Map
		this.listeners.set(key, listeners);
		// 返回一个取消订阅的函数(闭包)
		return () => {
			listeners.delete(callback);
			if (listeners.size === 0) {
				this.listeners.delete(key);
			}
		};
	}
	// 设置值并通知
	set(key, value) {
		const oldValue = this.data[key];
		this.data[key] = value;
		if (oldValue !== value) {
			this.notify(key, value, oldValue); //通知更新
		}
	}
	// 通知更新
	notify(key, newValue, oldValue) {
		// 获取该属性的所有监听器
		const listeners = this.listeners.get(key);
		if (listeners) {
			// 遍历所有监听器并执行回调函数
			listeners.forEach(callback => {
				callback(newValue, oldValue);
			});
		}
	}
}
// 使用示例
const store = new ReactiveStore({
	user: null,
	cart: [],
	settings: { theme: 'light' }
});
// 订阅用户变化
const unsubscribe = store.subscribe('user', (newUser, oldUser) => {
	console.log('用户变化:', oldUser?.name, '->', newUser?.name);
});
// 更新用户
store.set('user', { id: 1, name: '张三' });

六、高频面试题解析

问题1:Object.defineProperty 和 Proxy 的主要区别?

参考答案:
· 监测能力:Proxy 可以监测到对象的所有操作,包括新增、删除属性,数组索引修改等
· 性能方面:Proxy 是浏览器原生支持,性能更好,特别是在大型对象上
· 内存占用:Proxy 是按需代理,内存占用更优,defineProperty是一次劫持全部属性。
· API 设计:Proxy 提供 13 种拦截操作,更加强大和灵活,常见如get、set、has、deleteProperty、 setProperty、constructor、apply等。

Vue2发布时,主要浏览器在IE9+,Proxy的支持率大概才60%左右,考虑兼容性问题。Vue2 的成功证明了其技术选型的正确性——在正确的时间选择了正确的技术,满足了当时开发者的真实需求。
加分回答:可以提到 Vue2 为什么不用 Proxy,主要是兼容性问题(不支持 IE11)。

问题2:Vue3 的响应式相比 Vue2 有哪些性能优化?

参考答案:
· 初始化性能:Proxy 按需代理,避免了一开始就递归遍历所有属性
· 内存占用:使用 WeakMap 存储依赖关系,垃圾回收更友好
· 更新精度:可以精确知道哪个属性发生了变化,减少不必要的更新
· 开发体验:不再需要 Vue.set/delete 等特殊 API

问题3:如何手动实现一个简单的响应式系统?

思路:响应式编程的核心思想:数据变化自动触发相关更新
第一步:创建响应式包装器
创建一个Map存储依赖关系,使用Proxy包装原始对象,拦截get和set操作

第二步:实现依赖收集(get拦截)
当读取属性时,检查是否有"当前激活的副作用函数",若有将这个函数添加到该属性的依赖集合中,若没有则先创建,最后返回属性值

第三步:实现触发更新(set拦截)
设置新的属性值,检查该属性是否有依赖集合,如果有,遍历依赖集合中的所有副作用函数,逐个执行这些函数。

第四步:管理副作用函数
创建一个副作用函数包装器,在执行副作用函数前,将其设置为"当前激活的副作用函数";执行函数(执行期间会触发get,自动收集依赖),执行完毕后,清空"当前激活的副作用函数"。

function createReactive(obj) {
	// 创建一个 Map 对象来存储依赖关系
	const dependencies = new Map();
	return new Proxy(obj, {
		// 拦截对象的读取操作
		get(target, key) {
			// 检查是否有当前正在运行的副作用函数:通常在副作用函数执行时将其赋值给 activeEffect
			// 副作用函数:当数据变化时需要执行的函数
			// 收集依赖
			if (activeEffect) {
				if (!dependencies.has(key)) {
					dependencies.set(key, new Set());
				}
				dependencies.get(key).add(activeEffect);
			}
			return Reflect.get(target, key);
		},
		// 拦截对象的赋值操作
		set(target, key, value) {
			const result = Reflect.set(target, key, value);
			// 触发依赖
			if (dependencies.has(key)) {
				dependencies.get(key).forEach(effect => effect());
			}
			return result;
		}
	});
}
// 包装副作用函数
function effect(fn) {
	activeEffect = fn;
	fn();
	activeEffect = null;
}

七、总结

Vue 响应式的核心理念
数据驱动视图:数据变化自动更新 UI
依赖追踪:自动收集依赖,精确更新
性能平衡:在功能和性能间找到最佳平衡点

下期预告

下一篇我们将深入探讨 Vue 状态管理,包括 Pinia 的核心原理、Vuex 5 的新特性、状态管理的最佳实践,以及在大型项目中如何设计状态管理架构。状态管理是复杂应用的核心,不要错过哟

如果觉得有帮助,请关注+点赞+收藏,这是对我最大的鼓励! 如有问题,请评论区留言

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序媛小王ouc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值