我有个技术全面、经验丰富的小团队,可承接各类网站、跨端、小程序等项目,有需要可私信我
大家好, 今天我们来聊一聊 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 的新特性、状态管理的最佳实践,以及在大型项目中如何设计状态管理架构。状态管理是复杂应用的核心,不要错过哟
如果觉得有帮助,请关注+点赞+收藏,这是对我最大的鼓励! 如有问题,请评论区留言
749

被折叠的 条评论
为什么被折叠?



