1.在前面写的Javascript中,我们知道“万物皆对象”,对象的实际语义是由对象的内部方法指定的。所谓内部方法是指在对对象进行操作时,在引擎内部调用的方法,这些方法对用户是不可见的。如何区分一个对象是普通对象还是函数?对象可以通过内部方法和内部槽来区分。函数对象会部署方法[[call]],而普通对象不会。2、Proxy的工作原理当然,内部方法是多态的,不同类型的对象部署相同的内部方法,但它们的逻辑可能不同。如果创建代理对象时没有指定相应的拦截方法,那么当通过代理对象访问属性值时,代理的内部方法(如[[Get]])会调用原来的内部方法对象(例如[[Get]])来获取属性值,这将是代理透明的。Proxy也是一个对象,内部会部署很多方法在上面。当我们通过代理对象访问属性值时,会调用部署在代理对象上的内部方法[[Get]]。Proxy对象的内部方法:handler.apply()handler.construct()handler.defineProperty()handler.deleteProperty()handler.get()handler.getOwnPropertyDescriptor()handler.getPrototypeOf()handler.has()handler.isExtensible()handler.ownKeys()handler.preventExtensions()handler.set()handler.setPrototypeOf()当被代理对象是函数时,将部署另外两个内部方法[[Call]]和[[Constructor]]。当我们使用Proxy的deleteProperty()删除一个属性时,实际上是代理对象的内部方法和行为,只是改变了代理对象的属性值。如果要改变原始数据上的属性值,必须通过Reflect.deleteProperty(target,key)来实现。3、如何代理Object对象上一篇文章中使用get拦截方式读取属性其实是片面的,因为使用in运算符来检查对象的属性,使用for...in循环遍历对象都是对象的读操作。读取属性所有对普通对象的读取操作:访问属性:data.name。判断对象或原型上是否存在指定的key:keyindata。使用for...in遍历对象:for(constkeyindata){}。直接访问属性constdata={name:"pingping"}conststate=newProxy(data,{get(target,key,receiver){//跟踪函数建立副作用函数和代理对象track(target,key);//返回属性值Reflect.get(target,key,receiver);}})inoperatorconstdata={name:"pingping"}conststate=newProxy(data,{has(target,key,receiver){//跟踪函数建立副作用函数和代理对象之间的连接track(target,key);//返回属性值Reflect.has(target,key,receiver);}})for...in可以通过拦截ownKeys操作来实现对于for...in循环的间接拦截,在ownKeys中只能获取target对象target的所有key值,但不绑定具体的key。为此,需要使用Symbol构造一个唯一的键值来标识,即ITERATE_KEY。constdata={name:"pingping"}constITERATE_KEY=Symbol();conststate=newProxy(data,{ownKeys(target){//跟踪函数建立副作用函数和ITERATE_KEY之间的连接track(target,ITERATE_KEY);//返回属性值Reflect.ownKeys(target);}})Setproperties如果代理对象state只有一个property,for...in循环只会执行一次,但是当state加入一个新的property时,for...in就会执行多次。这是因为向对象添加新属性会触发与ITERATE_KEY关联的副作用函数的重新执行。constdata={name:"pingping"}constITERATE_KEY=Symbol();conststate=newProxy(data,{set(target,key,newVal){constres=Reflect.set(target,key,newVal,receiver);trigger(target,key);返回res;},ownKeys(target){//跟踪函数建立副作用函数和ITERATE_KEY之间的连接track(target,ITERATE_KEY);//返回属性值Reflect.ownKeys(target);}})effect(()=>{for(constkeyinstate){console.log(key);//name}})触发函数:functiontrigger(target,key){constdepsMap=bucket.get(target);如果(!depsMap)返回;consteffects=depsMap.get(key);constiterateEffects=depsMap.get(ITERATE_KEY);consteffectsToRun=newSet();//将与键关联的副作用函数添加到effectsToRuneffect&&effects.forEach(effectFn=>{if(effectFn!==activeEffect){effectsToRun.add(effectFn);}});//将与ITERATE_KEY关联的副作用函数添加到effectsToRuniterateEffects&&iterateEffects.forEach(effectFn=>{if(effectFn!==activeEffect){effectsToRun.添加(效果Fn);}});effectsToRun.forEach(effectFn=>{if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn);}else{effectFn();}});}上面的trigger函数中,添加属性时,在除了取出并执行与键值直接关联的副作用函数外,还需要取出并执行与ITERATE_KEY关联的那些副作用函数。在上面的代码中,添加一个新的对象的属性是可以做到的,但是对于修改现有对象的现有属性是不可行的。因为修改已有的属性值不会影响for...in循环,所以不管值怎么修改,循环只会执行一次。为此,不需要触发副作用函数的重新执行,否则会造成额外的性能开销。那么,我们应该如何应对呢?其实不管是给已有的对象增加新的属性,还是修改已有的属性,都是通过设置拦截函数来实现拦截的。因此,我们可以整合以上代码片段,在拦截设置操作时进行判断,判断该属性是否存在于当前对象上。如果是新属性,多次执行会触发ITERATE_KEY关联的副作用函数的执行。如果修改了属性,则无需触发执行与ITERATE_KEY关联的副作用函数。constTriggerType={SET:"SET",ADD:"ADD"};conststate=newProxy(data,{set(target,key,newVal){consttype=Object.prototype.hasOwnProperty.call(target,key)?TriggerType.SET:TriggerType.ADD;constres=Reflect.set(目标,key,newVal,receiver);//传入判断是否新增属性trigger(target,key,type);returnres;}})functiontrigger(target,key,type){constdepsMap=bucket.get(目标);如果(!depsMap)返回;consteffects=depsMap.get(key);consteffectsToRun=newSet();关联的副作用函数被添加到effectsToRuneffect&&effects.forEach(effectFn=>{if(effectFn!==activeEffect){effectsToRun.add(effectFn);}});if(type===TriggerType.ADD){constiterateEffects=depsMap.get(ITERATE_KEY);//将与ITERATE_KEY关联的副作用函数添加到effectsToRuniterateEffects&&iterateEffects.forEach(effectFn=>{if(effectFn!==activeEffect){effectsToRun.add(effectFn);}});}effectsToRun.forEach(effectFn=>{if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn);}else{effectFn();}});}删除代理对象中的属性,删除属性可以通过delete删除,则删除运算符依赖于Proxy对象的内部方法deleteProperty。同样,在删除指定属性时,需要检查当前属性是否在对象本身上,然后再考虑Reflect.deleteProperty函数来完成属性的删除。由于是操作代理对象的属性删除,会触发触发器的依赖收集操作,重新执行副作用函数。如果对象属性的数量减少,会影响for...in循环的次数,从而触发与ITERATE_KEY关联的副作用函数的重新执行。constTriggerType={SET:"SET",ADD:"ADD",DELETE:"DELETE"};conststate=newProxy(data,{deleteProperty(target,key){//检查要删除的属性是否在对象上consthadKey=Object.property.hasOwnProperty.call(target,key);//使用`Reflect.deleteProperty`函数删除属性constres=Reflect.deleteProperty(target,key);if(res&&hadKey){//只有删除成功才会触发更新trigger(target,key,"DELETE");}}})functiontrigger(target,key,type){constdepsMap=bucket.get(target);如果(!depsMap)返回;consteffects=depsMap.get(key);consteffectsToRun=newSet();//将与键关联的副作用函数添加到effectsToRuneffect&&effects.forEach(effectFn=>{if(effectFn!==activeEffect){effectsToRun.add(effectFn);}});if(type===TriggerType.ADD||type===TriggerType.DELETE){constiterateEffects=depsMap.get(ITERATE_KEY);//将关联ITERATE_KEY的副作用函数添加到iterateEffects中的effectsToRun&&iterateEffects.forEach(effectFn=>{if(effectFn!==activeEffect){effectsToRun.add(effectFn);}});}effectsToRun.forEach(effectFn=>{if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn);}else{effectFn();}});}4.合理的触发响应在前文中,它从规范的角度详细介绍了如何实现对象代理。同时,它需要清楚地知道操作类型来处理很多边界条件来触发响应,但也取决于触发响应时是否合理。当值没有变化时,不需要触发响应。对此,在修改set拦截函数的代码时,在调用trigger函数触发响应之前,需要检查该值是否真的发生了变化。constdata={name:"pingping"};conststate=newProxy(data,{set(target,key,newVal,receiver){//先获取旧值constoldVal=target[key];consttype=Object.prototype.hasOwnProperty.call(target,key)?"SET":"ADD";constres=Reflect.set(target,key,newVal,receiver);if(oldVal!==newVal){trigger(target,key,类型);}returnres})effect(()=>{console.log(state.name);});state.name="onechuan";调用set拦截函数时,需要先获取oldVal和新值newVal进行比较,只有当两者不相等时才会触发响应。那时,当oldVal和newVal的值都是NaN时,使用同余比较的结果为false。NaN===NaN//falseNaN!==NaN//true我们看到NaN值的比较值。当data.num的初始值为NaN时,后续修改为NaN作为新值。这个时候还是用fullvalue等待比较得到NaN!==NaN值为true,就会触发response函数,导致不必要的更新。为此,需要判断oldVal和newVal的值不为NaN,那么需要加上判断oldVal===oldVal||newVal===newVal,其实相当于Number.isNaN(newVal)||Number.isNaN(oldVal)。为了使用方便,我们封装了对象代理的功能。functionreactive(){returnnewProxy(data,{set(target,key,newVal,receiver){//先获取旧值constoldVal=target[key];consttype=Object.prototype.hasOwnProperty.call(target,key)?"SET":"ADD";constres=Reflect.set(target,key,newVal,receiver);if(oldVal!==newVal&&(oldVal===oldVal||newVal===newVal)){trigger(target,key,type);}returnres})}这样,当使用:constobj={};constdata={name:"pingping"}constparent=reactive(data);constchild=reactive(obj);//使用父对象作为子对象的原型对象Object.setPrototypeOf(child,parent);effect(()=>{console.log(child.name);//pingping});//修改child.name的值child.name="onechuan";//会导致副作用函数被重新执行两次在上面的代码中,会导致副作用函数被重新执行-执行了两次。其实处理就是用Proxy分别代理obj和data,把parent对象作为child的原型对象。当在副作用函数中读取child.name的值时,会触发子代理对象的get拦截函数,拦截函数的实现为Reflect.get(obj,"name",receiver)。但是子对象本身没有name属性,所以会获取对象的原型parent,调用原型的[[Get]]方法获取结果parent.name的值。而parent本身就是响应式数据,所以在sideeffectfunction中访问parent.name的值会导致sideeffectfunction被采集并建立响应链接。parent.name和child.name都会触发副作用函数的依赖集合,即它们都关联了副作用函数。重新分析上面的代码,当执行child.name=2时,会调用子对象的set拦截函数,set拦截函数的内部实现为Reflect.get(target,key,newVale,receiver)完成默认设置行为。由于child及其代理对象obj上没有name属性,所以会去原型parent中查找,即执行parent代理对象的set拦截函数。在读取child.name的值时,副作用函数不仅会被child.name触发,还会被parent.name收集。为此,当执行父代理对象的设置拦截函数时,会再次触发副作用函数实现。为此,副作用函数被执行了两次。那么,如何避免执行两次副作用函数呢?其实我们需要区分是谁触发了两次副作用函数的执行。其实我们只需要判断recevier是不是target的代理对象,然后执行parent.name触发的sideeffectfunctionblock即可。functionreactive(){returnnewProxy(data,{get(target,key,receiver){//代理对象可以通过raw属性访问数据if(key==="raw"){returntarget}track(target,key);returnReflect.get(target,key,receiver);},set(target,key,newVal,receiver){//先获取旧值constoldVal=target[key];consttype=Object.prototype.hasOwnProperty.call(target,key)?"SET":"ADD";constres=Reflect.set(target,key,newVal,receiver);//target===receiver.raw可以表明receiver是代理目标对象if(target===receiver.raw){if(oldVal!==newVal&&(oldVal===oldVal||newVal===newVal)){trigger(target,key,type);}}returnres})}在上面的代码中,我们添加了一个判断条件target===receiver.raw,只有一个为真,即recevier为target的代理对象时,触发更新,而由原型引起的更新可以被阻止,从而避免不必要的更新操作。5.写在上一篇,介绍了好哥们Proxy和Reflect的作用。本文介绍Proxy是如何实现Object对象的代理,并对代理对象进行设置值、获取值、删除属性等操作。介绍。还讨论了如何合理触发副作用函数的重新执行,屏蔽因原型更新导致的副作用函数不必要的重新执行。
