当前位置: 首页 > Web前端 > vue.js

vue3响应式与vue2的对比

时间:2023-03-31 20:26:31 vue.js

Vue官方说:“虽然没有完全遵循MVVM模型,但Vue的设计也受到了它的启发。”所以我们知道Vue框架有使用MVVM的想法。它的核心思想之一是数据驱动。数据驱动是指视图是由数据驱动生成的。如果我们要改变视图,我们必须通过修改数据来修改它,而不是像原来的JS那样直接操作DOM。反应灵敏?反应性是一种检测数据变化的机制。首先,让我们了解一下Vue是如何数据驱动的。这是Vue的官方图片。黄色那块是Vue负责渲染的render函数。当初始化和更新视图时将调用此渲染函数。而我们每次渲染的时候,都不可避免的会接触到我们的数据,也就是图中紫色的数据部分。渲染时,通过触发与数据相关的getter,将相关数据作为依赖收集到watcher中。(而这个watcher会在每个组件实例中对应一个)所以当我们后面修改收集到的依赖数据时,就会触发datasetter。setter方法会修改数据的值并通知(notify)给watcher,最后watcher触发render函数重新渲染视图。以上就是Vue实现数据驱动的过程。可见,实现数据驱动的关键是让我们的数据响应式,而getter和setter这两个方法就是让数据响应式的关键。但是我们在data里面写的原始数据,是不是有对应函数的getters和setters呢?其实这就是Vue的工作:重写数据的getter和setter。Vue2那么Vue是如何实现数据的响应式的呢?首先我们看一下Vue2的实现原理。Vue2主要是借用了Object.defineProperty()来重写数据的getters和setters。Object.defineProperty()的官方描述是:在一个对象上定义一个新的属性,或者修改一个对象已有的属性,并返回这个对象。一个简单的用法示例:letperson={}letname='xiaobai'Object.defineProperty(person,'personName',{get:function(){console.log('get方法被触发')returnname},set:function(val){console.log('triggeredthesetmethod')name=val}})//当读取到person对象的personName属性时,触发get方法console.log(person.personName)//当修改名字时,重新访问person.personName,发现修改成功'console.log(person.personName)这样我们就成功的监听到了person上name属性的变化。在这个例子中,我们只监听了一个属性的变化,但在现实中,我们通常需要同时监听多个属性的变化。这时候我们就需要使用Object.keys(obj)来遍历对象的所有属性。但是如果这时候单纯的把这两个API结合起来,就会出现问题:letperson={name='',age=0}Object.keys(person).forEach(function(key){Object.defineProperty(person,key,{enumerable:true,configurable:true,//默认传入get(){returnperson[key]},set(val){console.log(`for${key}attributeinpersonA已修改`)person[key]=val//修改后可以进行渲染操作}})})console.log(person.age)运行后发现错误:stackoverflow。这是因为当我们读取person的属性时,会触发get方法,返回person[key],但是访问person[key]也会触发get方法,导致递归调用,最终栈溢出.这时候我们需要设置一个中转Observer,让get中的返回值不直接访问obj[key]。letperson={name:'',age:0}//实现一个响应函数functiondefineProperty(obj,key,val){Object.defineProperty(obj,key,{get(){console.log(`visited${key}attribute`)returnval},set(newVal){console.log(`${key}attributehasbeenchangedto${newVal}`)val=newVal}})}//实现一个遍历函数ObserverfunctionObserver(obj){Object.keys(obj).forEach((key)=>{defineProperty(obj,key,obj[key])})}Observer(person)console.log(person.age)person.age=18console.log(person.age)现在实现了对对象属性的简单监控,但是如果我们要对对象进行深度监控,又该如何实现呢?其实在上面代码的基础上,加一个递归,就可以很容易的实现if(typeofval==='object'){observer(val)}Object.defineProperty(obj,key,{get(){console.log(`accessed${key}property`)returnval},set(newVal){console.log(`${key}属性已更改为${newVal}`)val=newVal}})}但是同时在观察者中加入递归停止条件:functionObserver(obj){//如果输入不是对象,则返回if(typeofobj!=="object"||obj===null){return}//for(keyinobj){Object.keys(obj).forEach((key)=>{defineProperty(obj,key,obj[key])})//}}到目前为止它看起来像这样通过Object.defineProperty()几乎可以实现响应式,但实际上通过Object.defineProperty()有一些缺陷:由于Object.defineProperty本质上是监控已有的属性,所以我们给对象添加或删除属性时无法检测到,因为对于这个原因是,在使用vue给data中的数组或者对象添加属性的时候,需要使用vm.$set来保证新增的属性也是响应式的。其实质就是手动监控新增的属性。虽然Object.definePropoerty()可以监听数组(但无法检测到push、unshift等增加数组索引的操作),在Vue2中出于性能的考虑,游玉玺并没有选择使用Object。defineProperty()来监控数组,而是通过重写Array上的原型方法来监控数组。如果你知道数组的长度,理论上可以为所有索引预先设置getters/setters。但是首先,在很多情况下你并不知道数组的长度。第二,如果是大数组,提前加上getter/setter,性能负担会很重。Vue3可以看到通过Object.definePorperty()进行数据监控比较麻烦,需要大量的人工处理。这也是为什么尤雨熙在Vue3.0中改用Proxy的原因。我们先来看看Proxy:Proxy用于为一个对象创建一个代理,从而实现对基本操作的拦截和定制。可以理解为:可以理解为在对象前设置一个“截距”。当被监控对象被访问时,必须经过这一层拦截。下面我们就来看看Proxy是如何解决之前Object.defineProperty()的一些缺陷的。不需要遍历属性,为了提高性能Object.defineProperty()需要遍历所有的属性,这就导致如果Vue对象的data/computed/props里面的数据很大,遍历会慢很多。由于Proxy代理的是整个对象,而不是像Object.defineProperty()那样代理对象的某个特定属性,所以不需要我们通过遍历一个一个进行数据绑定。//定义一个需要代理的对象letperson={age:0,school:'Xidian'}//定义handler对象lethander={get(obj,key){//如果对象有这个属性,它会返回属性值,如果没有,返回默认值66returnkeyinobj?obj[key]:66},set(obj,key,val){obj[key]=valreturntrue}}//传入handler对象ProxyletproxyObj=newProxy(person,hander)//测试get是否可以成功拦截console.log(proxyObj.age)//输出0console.log(proxyObj.school)//输出Xidianconsole.log(proxyObj.name)//输出默认值66//测试set是否能成功拦截proxyObj。age=18console.log(proxyObj.age)//输出18修改不需要手动监听对象的新属性,和之前一样,因为Proxy代表了整个对象,自然可以监听变化对象的新属性和已删除属性。这样就不需要像使用Object.defineProperty()那样手动给新属性添加监视器。深度监控的性能优化因为Object.defineProperty()的深度监控是一次全部监控,而proxy的深度监控只有在真正读取/操作数据时才会递归,起到了延迟加载,大大提高了性能。不需要重写数组的原型方法。前面提到,由于Object.defineProperty()无法监听添加数组索引的操作,加上性能的考虑,vue2通过重写数组的方式来拦截该操作。但是proxy可以直接拦截数组,不需要一次性监听所有的数据,所以不会对性能造成太大的影响。Proxy支持多种拦截操作。相对于Object.defineProperty只支持两种操作拦截,Proxy支持的操作拦截尤为丰富。P支持13种类型:get(target,propKey,receiver):拦截对象属性的读取,如proxy.foo和proxy['foo']。set(target,propKey,value,receiver):拦截对象属性的设置,比如proxy.foo=v或者proxy['foo']=v,返回一个布尔值。has(target,propKey):拦截propKey在proxy中的操作,返回一个布尔值。deleteProperty(target,propKey):拦截deleteproxy[propKey]操作,返回一个布尔值。ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回数组。该方法返回目标对象的所有属性的属性名,而Object.keys()的返回结果只包含目标对象本身的可遍历属性。getOwnPropertyDescriptor(target,propKey):拦截Object.getOwnPropertyDescriptor(proxy,propKey),返回属性的描述对象。defineProperty(target,propKey,propDescs):拦截Object.defineProperty(proxy,propKey,propDescs),Object.defineProperties(proxy,propDescs),返回一个布尔值。preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。setPrototypeOf(target,proto):拦截Object.setPrototypeOf(proxy,proto),返回一个布尔值。如果目标对象是一个函数,那么还有两个额外的动作可以被拦截。apply(target,object,args):拦截Proxy实例作为函数调用的操作,如proxy(...args),proxy.call(object,...args),proxy.apply(...).construct(target,args):拦截调用一个Proxy实例作为构造函数的操作,比如newproxy(...args)。参考:一篇文章看懂Vue3.0为什么使用Proxy