本人承接各种网站、跨端、小程序等开发项目,有需要可私信我
大家好, 今天我们来聊一聊 Vue 框架中最核心的部分——响应式系统。
引言:为什么响应式是 Vue 的灵魂?
响应式是Vue的灵魂,因为它实现了Vue最核心的承诺:数据驱动视图。
这不仅仅是技术实现,更是根本性的设计哲学,直观的数据、视图绑定,虚拟的DOM实现,框架内所有API的支撑等。
响应式让Vue从"一个UI库"变成了"一套思维范式"。它决定了你如何思考问题(关注数据状态而非DOM操作),而不仅仅是如何写代码。
文章目录
- 引言:为什么响应式是 Vue 的灵魂?
- 一、响应式系统演进史
- 1.1 初代响应式:脏值检测(AngularJS)
- 1.2 Vue1 的细粒度响应式
- 1.3 Vue2 的改进:依赖收集与批量更新
- 二、Vue2 Object.defineProperty 深度解析
- 2.1 Object.defineProperty
- 2.2 存在的局限性
- 2.3 总结
- 三、Vue3 Proxy:新响应式方案
- 3.1 Proxy代理
- 3.2 存在的局限性
- 3.3 总结:Proxy 的时代意义
- 四、defineProperty VS Proxy 性能优化的全方位对比
- 4.1 内存优化:按需代理 vs 全量劫持
- 4.2 性能基准
- 五、Proxy 在实际项目中的应用
- 5.1 Vue3 响应式源码解析
- 5.2 自定义响应式工具库
- 六、高频面试题解析
- 问题1:Object.defineProperty 和 Proxy 的主要区别?
- 问题2:Vue3 的响应式相比 Vue2 有哪些性能优化?
- 问题3:如何手动实现一个简单的响应式系统?
- 七、总结
- 下期预告
一、响应式系统演进史
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 响应式理念newVue({data:{user:{name:'张三'}},watch:{'user.name':function(val){console.log('姓名变化:',val);}}});1.3 Vue2 的改进:依赖收集与批量更新
Vue2 引入了虚拟 DOM 和组件级响应式。
// Vue2 组件级更新exportdefault{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函数)**来理解原理。
functiondefineReactive(obj,key,val){// 每个属性都创建一个"依赖收集器"constdeps=[];Object.defineProperty(obj,key,{enumerable:true,configurable:true,get(){// 收集依赖:当前有"观察者"在读取这个属性吗?if(currentWatcher){// 记录:这个观察者依赖我deps.push(currentWatcher);}returnval;},set(newVal){if(val===newVal)return;val=newVal;// 更新:我的值变了,通知所有依赖我的观察者deps.forEach(watcher=>{watcher.update();// 触发更新});}});}// 遍历对象使其响应式functionobserve(obj){if(typeofobj!=='object'||obj===null){return;}Object.keys(obj).forEach(key=>{defineReactive(obj,key,obj[key]);});}// 使用constdata={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 实现响应式functionreactive(target){// 返回一个代理对象returnnewProxy(target,{// 拦截读方法get(target,key,receiver){// Reflect 会保持对象的所有约束(如不可写属性、setter等)constres=Reflect.get(target,key,receiver);track(target,key);// 收集依赖// 如果结果是对象,则递归转为响应式returntypeofres==='object'?reactive(res):res;},// 拦截写方法set(target,key,value,receiver){constoldValue=target[key];constresult=Reflect.set(target,key,value,receiver);if(result&&oldValue!==value){trigger(target,key);// 触发更新}returnresult;},// 拦截删除属性方法deleteProperty(target,key){consthadKey=hasOwn(target,key);constresult=Reflect.deleteProperty(target,key);if(result&&hadKey){trigger(target,key);// 触发更新}returnresult;}});}// 依赖收集consttargetMap=newWeakMap();functiontrack(target,key){if(activeEffect){letdepsMap=targetMap.get(target);if(!depsMap){targetMap.set(target,(depsMap=newMap()));}letdep=depsMap.get(key);if(!dep){depsMap.set(key,(dep=newSet()));}dep.add(activeEffect);}}// 依赖触发functiontrigger(target,key){constdepsMap=targetMap.get(target);if(!depsMap)return;//consteffects=depsMap.get(key);effects&&effects.forEach(effect=>effect());}3.2 存在的局限性
· Proxy 只能代理对象,不能代理基本类型(如字符串、数字、布尔值)。
· Proxy 的 this 问题:在 Proxy 的 handler 方法中,this 指向的是 handler 对象,而不是被代理的目标对象。因此,在需要访问目标对象时,通常使用第一个参数 target。
constobj={name:'Vue',getName(){returnthis.name}// this 指向谁?}constproxy=newProxy(obj,{get(target,key,receiver){// receiver 是 proxy 本身constvalue=Reflect.get(target,key,receiver);if(typeofvalue==='function'){// 绑定正确的 thisreturnvalue.bind(receiver)}returnvalue}})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.definePropertyfunctiondefineAllProperties(obj){// 循环挟持多有属性Object.keys(obj).forEach(key=>{defineReactive(obj,key,obj[key]);});// 对于嵌套对象Object.keys(obj).forEach(key=>{if(typeofobj[key]==='object'){defineAllProperties(obj[key]);// 递归劫持}});}// Proxyconstproxy=newProxy(obj,{get(target,key){constvalue=target[key];if(typeofvalue==='object'&&value!==null){// 只有访问到时才代理嵌套对象returnreactive(value);}returnvalue;}});4.2 性能基准
编写测试代码来进行性能测试。
// 测试代码consttestData={/* 包含1000个属性的对象 */};// Object.defineProperty 版本console.time('defineProperty');constobserved1=observeWithDefineProperty(testData);console.timeEnd('defineProperty');// Proxy 版本console.time('proxy');constobserved2=observeWithProxy(testData);console.timeEnd('proxy');// 访问性能测试 definePropertyconsole.time('access-defineProperty');for(leti=0;i<10000;i++){observed1['prop'+(i%1000)];}console.timeEnd('access-defineProperty');// 访问性能测试 proxyconsole.time('access-proxy');for(leti=0;i<10000;i++){observed2['prop'+(i%1000)];}console.timeEnd('access-proxy');初始化:Proxy 稍慢,但可接受
访问速度:两者相当
内存占用:Proxy 明显更低
五、Proxy 在实际项目中的应用
5.1 Vue3 响应式源码解析
下面是简化版的Vue3的响应式核心
functioncreateReactiveObject(target,baseHandlers){// isObject 是类型检查函数,判断是否为对象或数组if(!isObject(target)){returntarget;}// 从 proxyMap 中查找是否已经有该对象的代理constexistingProxy=proxyMap.get(target);if(existingProxy){returnexistingProxy;}// 不存在的则创建新的 Proxy 代理对象constproxy=newProxy(target,baseHandlers);proxyMap.set(target,proxy);returnproxy;}// 基本处理器constmutableHandlers={get(target,key,receiver){// 依赖收集track(target,key);constres=Reflect.get(target,key,receiver);// 自动解包 refif(isRef(res)){returnres.value;}// 深层响应式// 只在访问对象属性时才递归转换为响应式, 惰性代理if(isObject(res)){returnreactive(res);}returnres;},set(target,key,value,receiver){constoldValue=target[key];// 处理 ref 赋值if(isRef(oldValue)&&!isRef(value)){oldValue.value=value;returntrue;}// 检查属性是否已经存在(用于区分新增和修改)// hasOwn 检查对象自身是否有该属性(不包括原型链)consthadKey=hasOwn(target,key);constresult=Reflect.set(target,key,value,receiver);// 只有对象变化时才触发更新// 确保触发更新的是原始对象,而不是其他代理对象,toRaw 获取代理对象的原始对象, 这个检查避免重复触发更新if(target===toRaw(receiver)){if(!hadKey){trigger(target,key,TriggerOpTypes.ADD);}elseif(hasChanged(value,oldValue)){trigger(target,key,TriggerOpTypes.SET);}}returnresult;}};5.2 自定义响应式工具库
// 响应式状态管理器classReactiveStore{// 构造函数constructor(data={}){// 使用 reactive 函数将传入的数据转换为响应式数据this.data=reactive(data);// 初始化一个 Map 来存储监听器,key: 属性名;value: 该属性的监听函数集合(Map)this.listeners=newMap();}// 订阅特定字段subscribe(key,callback){// 获取该属性已有的监听器集合,如果没有则创建新的 Setconstlisteners=this.listeners.get(key)||newSet();// 将新的回调函数添加到监听器集合中listeners.add(callback);// 将更新后的监听器集合保存回 Mapthis.listeners.set(key,listeners);// 返回一个取消订阅的函数(闭包)return()=>{listeners.delete(callback);if(listeners.size===0){this.listeners.delete(key);}};}// 设置值并通知set(key,value){constoldValue=this.data[key];this.data[key]=value;if(oldValue!==value){this.notify(key,value,oldValue);//通知更新}}// 通知更新notify(key,newValue,oldValue){// 获取该属性的所有监听器constlisteners=this.listeners.get(key);if(listeners){// 遍历所有监听器并执行回调函数listeners.forEach(callback=>{callback(newValue,oldValue);});}}}// 使用示例conststore=newReactiveStore({user:null,cart:[],settings:{theme:'light'}});// 订阅用户变化constunsubscribe=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,自动收集依赖),执行完毕后,清空"当前激活的副作用函数"。
functioncreateReactive(obj){// 创建一个 Map 对象来存储依赖关系constdependencies=newMap();returnnewProxy(obj,{// 拦截对象的读取操作get(target,key){// 检查是否有当前正在运行的副作用函数:通常在副作用函数执行时将其赋值给 activeEffect// 副作用函数:当数据变化时需要执行的函数// 收集依赖if(activeEffect){if(!dependencies.has(key)){dependencies.set(key,newSet());}dependencies.get(key).add(activeEffect);}returnReflect.get(target,key);},// 拦截对象的赋值操作set(target,key,value){constresult=Reflect.set(target,key,value);// 触发依赖if(dependencies.has(key)){dependencies.get(key).forEach(effect=>effect());}returnresult;}});}// 包装副作用函数functioneffect(fn){activeEffect=fn;fn();activeEffect=null;}七、总结
Vue 响应式的核心理念
数据驱动视图:数据变化自动更新 UI
依赖追踪:自动收集依赖,精确更新
性能平衡:在功能和性能间找到最佳平衡点
下期预告
下一篇我们将深入探讨Vue 状态管理,包括 Pinia 的核心原理、Vuex 5 的新特性、状态管理的最佳实践,以及在大型项目中如何设计状态管理架构。状态管理是复杂应用的核心,不要错过哟
如果觉得有帮助,请关注+点赞+收藏,这是对我最大的鼓励! 如有问题,请评论区留言