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

日常题中vue数据劫持的原理是什么?_4

时间:2023-03-31 21:25:25 vue.js

什么是数据劫持?定义:数据劫持是指在访问或修改对象的某个属性、执行附加操作或修改返回结果时,通过一段代码拦截这种行为。简单来说,当我们触发函数的时候,我们做了一些我们想做的事情,也就是所谓的“劫持”操作数据劫持的两种方案:Object.definePropertyProxy1).Object.defineProperty语法:Object.defineProperty(obj,prop,descriptor)参数:obj:目标对象prop:需要定义的属性或方法的名称descriptor:目标属性拥有的属性和可以定义的属性列表:value:可写属性的值:如果为false,则不能覆盖该属性的值。get:一旦访问到目标属性,就会回调该方法,并将该方法的操作结果返回给用户。set:一旦设置了目标属性,就会回调此方法。configurable:如果为false,任何试图删除目标属性或修改该属性的后续属性(可写、可配置、可枚举)的行为都将失效。enumerable:是否可以在for...in循环中遍历或者在Object.keys中列出。Vue中的例子是通过Object.defineProperty劫持对象属性的setter和getter操作,“植入”一个监听器,在数据发生变化时发送通知,如下:vardata={name:'test'}Object.keys(data).forEach(function(key){Object.defineProperty(data,key,{enumerable:true,configurable:true,get:function(){console.log('get');},set:function(){console.log('监控到数据有变化');}})});data.name//控制台会打印“get”data.name='hxx'//控制台会打印从上面的“监听到数据已经改变”的例子,我们可以看到我们完全可以控制设置和读取对象属性。在Vue中,Object.defineProperty方法在很多地方都用的非常巧妙。用在什么地方,解决什么问题,简单说一下:监听对象属性的变化。添加到订阅者dep的属性,在数据更改时发送通知。相关源码如下:(作者使用ES6+flow编写,代码在src/core/observer/index.js模块)导出函数defineReactive(obj:Object,key:string,val:any,customSetter?:Function){constdep=newDep()//创建一个订阅对象constproperty=Object.getOwnPropertyDescription//在属性的描述中,如果configurable为false,则对该属性的任何修改都将无效if(property&&property.configurable===false){return}scriptor(obj,key)//获取obj对象关键属性的描述//迎合预定义的getter/settersconstgetter=property&&property.getconstsetter=property&&property.setletchildOb=observe(val)//创建观察者对象Object.defineProperty(obj,key,{enumerable:true,//enumerableconfigurable:true,//可修改get:functionreactiveGetter(){constvalue=getter?getter.call(obj):val//先调用默认的get方法获取值//get方法od在这里被劫持了,这也是作者的一个巧妙设计。创建watcher实例时,通过调用对象的get方法将其添加到订阅者dep创建的watcher实例}if(Array.isArray(value)){dependArray(value)}}returnvalue//返回属性值},set:functionreactiveSetter(newVal){constvalue=getter?getter.call(obj):val//先取旧值if(newVal===value){return}//这个是用来判断生产环境的,可以忽略if(process.env.NODE_ENV!=='production'&&customSetter){customSetter()}if(setter){setter.call(obj,newVal)}else{val=newVal}childOb=observe(newVal)//继续监听新的属性值dep.notify()//这才是劫持的真正目的,发送给订阅者通知}})}上面是Vue的监控对象属性的变化,那么问题来了,我们经常传递数据的时候,往往不是一个对象,它是很可能是一个数组,所以没办法,答案很明显,否则那我们看看作者是如何监听数组的变化的:监听数组的变化,看代码:constarrayProto=Array.prototype//prototypeofnativeArrayexportconstarrayMethods=Object.create(arrayProto);','shift','unshift','splice','sort','reverse'].forEach(function(method){constoriginal=arrayProto[method]//缓存元素数组的原型//数组被重写hereSeveralprototypemethodsdef(arrayMethods,method,functionmutator(){//这里备份一份参数应该是出于性能考虑leti=arguments.lengthconstargs=newArray(i)while(i--){args[i]=arguments[i]}constresult=original.apply(this,args)//原方法求值constob=this.__ob__//这里this.__ob__指向数据let插入的Observerswitch(method){case'push':inserted=argsbreakcase'unshift':inserted=argsbreakcase'splice':inserted=args.slice(2)break}if(inserted)ob.observeArray(inserted)//通知变化ob.dep.notify()返回result})})...//定义属性functiondef(obj,key,val,enumerable){Object.defineProperty(obj,key,{value:val,enumerable:!!enumerable,writable:true,configurable:true});}详见前端高级面试题。上面的代码主要是继承Array本身的原型方法,然后进行劫持修改。你可以发送一个通知,Vue会在观察者数据阶段判断如果是一个数组,然后修改这个数组。这样就可以在劫持过程中控制对阵列的任何后续操作。结合Vue的思想,简单写个小demo,便于更好理解:(arrayMethod,method,{value:function(){leti=arguments.lengthletargs=newArray(i)while(i--){args[i]=arguments[i]}letoriginal=Array.prototype[method];letresult=original.apply(this,args);console.log("已经可以控制了,哈哈");returnresult;},enumerable:true,writable:true,configurable:true})})letbar=[1,2];bar.__proto__=arrayMethod;bar.push(3);//控制台会打印"Alreadyundercontrol,哈哈";成员“3”已成功添加到栏中。整个过程看起来好像没什么问题。看来Vue已经做到了极致。事实上,Vue仍然无法检测到数据项和数组长度的变化。例如下面的调用:vm.items[index]="xxx";vm.items.length=100;所以我们尽量避免这样的调用方式。如果实在需要,作者还帮我们实现了一个$set操作。往下理解对象属性代理的实现。一般情况下,我们实例化一个Vue对象是这样的:varVM=newVue({data:{name:'lhl'},el:'#id'})按理说我们操作的是data当时应该是VM.data.name='hxx',但是作者认为这样不够简洁,所以VM.name='hxx'可能的相关代码通过代理实现如下:functionproxy(vm,key){if(!isReserved(key)){Object.defineProperty(vm,key,{configurable:true,enumerable:true,get:functionproxyGetter(){returnvm._data[key]},set:functionproxySetter(val){vm._data[key]=val;}});}}表面上看我们是在操作VM.name,实际上是通过劫持Object.defineProperty()中的get和set方法来实现的。Object.defineProperty()的缺点1).无法监听数组变化letarr=[1,2,3]letobj={}Object.defineProperty(obj,'arr',{get(){console.log('getarr')returnarr},set(newVal){console.log('set',newVal)arr=newVal}})obj.arr.push(4)//只打印getarr,不打印setobj。arr=[1,2,3,4]//以下可以正常设置数组的方法不会触发set:push,pop,shift,unshift,splice,sort,reverseVue将这些方法定义为变异方法(mutationmethod),这是指修改原始数组的方法。与之对应的是非变异方法(non-mutatingmethod),如filter、concat、slice等,它们不会修改原数组,而是返回一个新数组。2).必须使用Object.defineProperty()来遍历对象的每个属性。大部分需要配合Object.keys()进行遍历,所以多了一层嵌套。如:Object.keys(obj).forEach(key=>{Object.defineProperty(obj,key,{//...})})3).所谓嵌套对象,一定要深入遍历。指的是类似letobj={info:{name:'eason'}}如果是这种嵌套对象,则必须逐层遍历,直到为每个对象的每个属性调用Object.defineProperty()。给出完整版的数据劫持代码:constarrayProto=Array.prototype;//获取原型上的方法constproto=Object.create(arrayProto)//复制原型上的方法;['push','shift','pop','splice'].forEach(method=>{//console.log(method)//重写'push','shift','pop','splice',当然可以加一个更多方法,添加任何你想要的proto[method]=function(...args){//console.log(this)//[1,2,3,{age:[Getter/Setter]}]updateView();arrayProto[method].call(this,...args)}})functionupdateView(){console.log("更新视图成功...")}functionobserver(obj){if(typeofobj!=="object"||obj==null){returnobj}if(Array.isArray(obj)){//如果是数组,重写数组原型上的方法Object.setPrototypeOf(obj,proto)for(leti=0;i