什么是代理模式 代理模式是一种结构化设计模式(小灰的文章认为也可以算作是行为型设计模式),代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用 ,这就是代理模式的定义。通俗的讲,代理模式的核心是在被调用方和调用方之间增加一个中介者的角色,也就是代理。
图源自小灰的文章”什么是代理模式?”
现实生活中,比如我们有租房需求,可能就需要经过房屋中介,让我们认识能够找到合适的房东。
在求职高薪岗位的时候,我们也需要找到猎头,给我们推荐合适的公司。
代理模式在现实生活中无处不在……
当然,你觉得代理模式可能会让简单的事情变复杂,但中介者的角色实际上会给你减少很多麻烦和成本,在代码中代理模式可以避免对业务类的侵入,把日志、事务之类和业务无关的辅助功能单独拎出来。
代理模式有以下两个优点:
中介隔离作用:
在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
符合代码设计的开放-封闭原则:
代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开放-封闭原则 。
代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是通过调用委托类的相关方法,来提供特定的服务。
真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
代理模式虽然和装饰者模式很相似,但是装饰器模式会对装饰对象增加功能,而代理模式并不会对源对象有改变,从外层去操作了这个对象,对象本身是不会有其他的改变。
代理模式的缺点主要是增加了系统的复杂度,要斟酌当前场景是不是真的需要引入代理模式。
实现代理模式 这里用《JavaScript设计模式与开发实践》一书中的例子。
小明想要追一个小姐姐,想给小姐姐送一束花表白。在不使用代理模式和使用代理模式的简单例子。
不使用代理模式 ES5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var Flower = function ( ){};var xiaoming = { sendFlower : function ( target ){ var flower = new Flower (); target.receiveFlower ( flower ); } };var cuteGirl = { receiveFlower : function ( flower ){ console .log ( '收到花 ' + flower ); } }; xiaoming.sendFlower ( cuteGirl );
ES6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Flower { }let xiaoming = { sendFlower (target ){ let flower = new Flower (); target.receiveFlower ( flower ); } }let cuteGirl = { receiveFlower (flower ){ console .log ( '收到花 ' + flower ); } } xiaoming.sendFlower ( cuteGirl );
使用代理模式 以上就是不使用代理模式的例子,小明在不了解小姐姐喜好的情况下,贸然直接表白被拒绝的概率非常大,但是小明的好朋友恰好认识小姐姐的舍友,舍友会在小姐姐心情好的时候,帮小明把鲜花转交给小姐姐。
ES5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 var Flower = function ( ){};var xiaoming = { sendFlower : function ( target ){ var flower = new Flower (); target.receiveFlower ( flower ); } };var goodFriend = { receiveFlower : function ( flower ){ cuteGirl.listenGoodMood (function ( ){ cuteGirl.receiveFlower ( flower ); }); } };var cuteGirl = { receiveFlower : function ( flower ){ console .log ( '收到花 ' + flower ); }, listenGoodMood : function ( fn ){ setTimeout (function ( ){ fn (); }, 10000 ); } }; xiaoming.sendFlower ( goodFriend );
ES6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Flower { }let xiaoming = { sendFlower (target ){ let flower = new Flower (); target.receiveFlower ( flower ); } };let goodFriend = { receiveFlower (flower ){ cuteGirl.listenGoodMood (() => { cuteGirl.receiveFlower ( flower ); }); } };let cuteGirl = { receiveFlower ( flower ){ console .log ( '收到花 ' + flower ); }, listenGoodMood ( fn ){ setTimeout (() => { fn (); }, 10000 ); } }; xiaoming.sendFlower ( goodFriend );
使用代理模式中的保护代理 保护代理用于对象应该具有不同访问权限的场景,控制对原始对象的访问。
还是用上述书中的例子,因为小明和小姐姐的舍友是好朋友,她了解小明的为人,所以愿意为小明转送鲜花给小姐姐。
而如果把小明换成一个不相干的人,那么小姐姐的舍友不可能会答应这个奇怪的请求。
但是在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。所以我们对下面的例子进行简单的改造,给鲜花类增加来源,从而实现简单的保护代理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class Flower { constructor (source ) { this .source = source; } }let xiaoming = { sendFlower (target ){ let flower = new Flower ('xiaoming' ); target.receiveFlower ( flower ); } };let passerby = { sendFlower (target ){ let flower = new Flower ('passerby' ); target.receiveFlower ( flower ); } };let ladybro = { receiveFlower (flower ){ if (flower.source === 'xiaoming' ) { cuteGirl.listenGoodMood (() => { cuteGirl.receiveFlower ( flower ); }); } else { throw new Error ('小姐姐的闺蜜拒绝帮你送花!' ) } } };let cuteGirl = { receiveFlower ( flower ){ console .log ( '收到花 ' + flower ); }, listenGoodMood ( fn ){ setTimeout (() => { fn (); }, 10000 ); } }; xiaoming.sendFlower ( ladybro ); passerby.sendFlower ( ladybro );
使用代理模式中的虚拟代理 还是用上面书中的例子,鲜花的种类有很多种,每种鲜花的售价也不近相同,不同的鲜花也有不同的保质期。
小明为了夺得小姐姐的欢心,希望小姐姐的闺蜜在小姐姐心情好的时候,再去帮忙购买一束比较昂贵的鲜花转送给小姐姐,此时的操作就叫虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 let ladybro = { receiveFlower (flower ){ if (flower.source === 'xiaoming' ) { cuteGirl.listenGoodMood (() => { let flower = new Flower ('xiaoming' ); cuteGirl.receiveFlower ( flower ); }); } else { throw new Error ('小姐姐的闺蜜拒绝帮你送花!' ) } } };
常见的虚拟代理实现 图片预加载 这里也是引用书中的例子,常见的开发需求之一,在图片未加载回来之前,希望有一个loading图进行占位,等loading图加载回来后再填充到img节点。
未使用代理模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 let MyImage = (function ( ){ let imgNode = document .createElement ( 'img' ); document .body .appendChild ( imgNode ); let img = new Image ; img.onload = function ( ){ imgNode.src = img.src ; }; return { setSrc : function ( src ){ imgNode.src = 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' ; img.src = src; } } })();MyImage .setSrc ( 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );
以上是未使用代理模式的写法,这也是常常容易写出来的代码情况,它在实现业务上并没有什么问题,但是MyImage
对象除了负责给img
节点设置src
外,还要负责预加载图片,违反了面向对象设计的原则——单一职责原则。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
它同时还违反了开放—封闭原则,根据开放—封闭原则:
软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
英文预加载loading的这个功能,是耦合进MyImage
对象里的,如果以后某个时候,我们不需要预加载显示loading这个功能了,就只能在MyImage
对象里面改动代码。虽然MyImage
改动代码只需要几行就可以解决问题,但是换做其他甚至拥有10万行代码级别的JavaScript项目,要修改它的源代码风险就很大了。
使用代理模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 let myImage = (function ( ){ let imgNode = document .createElement ( 'img' ); document .body .appendChild ( imgNode ); return { setSrc : function ( src ){ imgNode.src = src; } } })();let proxyImage = (function ( ){ let img = new Image ; img.onload = function ( ){ myImage.setSrc ( this .src ); } return { setSrc : function ( src ){ myImage.setSrc ( 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' ); img.src = src; } } })(); proxyImage.setSrc ( 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );
在使用了代理模式后:
图片本地对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口;
代理对象负责在图片未加载完成之前,引入预加载的loading图,负责了图片预加载的功能;
同时,它也满足了开放—封闭原则的基本思想:
开放—封闭原则的基本思想:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。
我们并没有改变或者增加MyImage
的接口,但是通过代理对象,实际上给系统添加了新的行为(这里的行为是图片预加载)。
合并HTTP请求 这里也是引用一个书中的例子,例如我们需要做一个文件同步的功能,在选中对应的文件时,需要被同步到自己的OneDrive。
这里把OneDrive中的同步文件夹替换成网页中的checkbox
1 2 3 4 5 6 7 8 9 10 11 <body > <input type ="checkbox" id ="1" > </input > 1 <input type ="checkbox" id ="2" > </input > 2 <input type ="checkbox" id ="3" > </input > 3 <input type ="checkbox" id ="4" > </input > 4 <input type ="checkbox" id ="5" > </input > 5 <input type ="checkbox" id ="6" > </input > 6 <input type ="checkbox" id ="7" > </input > 7 <input type ="checkbox" id ="8" > </input > 8 <input type ="checkbox" id ="9" > </input > 9</body >
未使用代理模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let synchronousFile = function ( id ){ console .log ( '开始同步文件,id为: ' + id ); };let checkbox = document .getElementsByTagName ( 'input' );for ( let i = 0 , c; c = checkbox[ i++ ]; ){ c.onclick = function ( ){ if ( this .checked === true ){ synchronousFile ( this .id ); } } };
在未使用代理模式时,每选中一次checkbox,就会触发一次同步文件请求,频繁的网络请求,会给服务器带来比较大的开销,此时我们可以在不改变synchronousFile
函数职能的情况下,将它进行代理。
使用代理模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 let synchronousFile = function ( id ){ console .log ( '开始同步文件,id为: ' + id ); };let proxySynchronousFile = (function ( ){ let cache = [], timer; return function ( id ){ cache.push ( id ); if ( timer ){ return ; } timer = setTimeout (function ( ){ synchronousFile ( cache.join ( ',' ) ); clearTimeout ( timer ); timer = null ; cache.length = 0 ; }, 2000 ); } })();let checkbox = document .getElementsByTagName ( 'input' );for ( let i = 0 , c; c = checkbox[ i++ ]; ){ c.onclick = function ( ){ if ( this .checked === true ){ proxySynchronousFile ( this .id ); } } };
synchronousFile
函数被代理后的函数我们起名为proxySynchronousFile
,它增加了一个缓存数组,所有两秒内的checkbox选中,都会被添加到缓存数组check中,等待2秒之后才把这2秒之内需要同步的文件ID一次性全打包发给服务器(将多个id拼接成逗号分割的字符串),在实时性要求不是很高的系统,这能大大减少服务器的压力。
惰性加载中的应用 来自于书中的例子,假设有一个迷你控制台的项目——miniConsole.js,它有一个log函数,专门用于打印参数。
1 2 3 4 5 6 7 8 9 10 let miniConsole = { log : function ( ){ console .log ( Array .prototype .join .call ( arguments ) ); } };export default miniConsole
因为这个控制台项目,是只在控制台展示的时候才需要的,我们希望他在有必要的时候才开始加载它,比如按F2的时候,加载miniConsole.js,就可以使用代理模式,惰性加载miniConsole.js。
大致的步骤是:
在用户敲击F2的时候,才去动态引入miniConsole.js的script标签
在用户敲击F2之前执行过的log命令,都会被缓存到代理对象内部的cache缓存数组内
等动态引入miniConsole.js的操作完成后,再从中逐一取出并执行。
详细代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 let proxyMiniConsole = (function ( ){ let cache = []; let handler = function ( ev ){ if ( ev.keyCode === 113 ){ let script = document .createElement ( 'script' ); script.src = 'miniConsole.js' ; document .getElementsByTagName ( 'head' )[0 ].appendChild ( script ); document .body .removeEventListener ( 'keydown' , handler ); script.onload = function ( ){ for ( var i = 0 , fn; fn = cache[ i++ ]; ){ fn (); } }; } }; document .body .addEventListener ( 'keydown' , handler, false ); return { log : function ( ){ let args = arguments ; cache.push ( function ( ){ return miniConsole.log .apply ( miniConsole, args ); }); } } })(); miniConsole.log ( 11 );
使用代理模式中的缓存代理 缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
计算乘积 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 let mult = function ( ){ let a = 1 ; for ( let i = 0 , l = arguments .length ; i < l; i++ ){ a = a * arguments [i]; } return a; };let plus = function ( ){ let a = 0 ; for ( let i = 0 , l = arguments .length ; i < l; i++ ){ a = a + arguments [i]; } return a; };let createProxyFactory = function ( fn ){ let cache = {}; return function ( ){ let args = Array .prototype .join .call ( arguments , ',' ); if ( args in cache ){ return cache[args]; } return cache[args] = fn.apply ( this , arguments ); } };let proxyMult = createProxyFactory ( mult ), proxyPlus = createProxyFactory ( plus );console .log ( proxyMult ( 1 , 2 , 3 , 4 ) ); console .log ( proxyMult ( 1 , 2 , 3 , 4 ) ); console .log ( proxyPlus ( 1 , 2 , 3 , 4 ) ); console .log ( proxyPlus ( 1 , 2 , 3 , 4 ) );
ES6中的代理模式 使用ES6 Proxy API实现虚拟代理 图片预加载 虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
在先前的代码中,展示了ES5如何实现图片预加载,在ES6引入了Proxy API后,也可以利用它实现同样的图片预加载需求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const createImgProxy = (img, loadingImg, realImg ) => { let hasLoaded = false ; const virtualImg = new Image (); virtualImg.src = realImg; virtualImg.onload = () => { Reflect .set (img, 'src' , realImg); hasLoaded = true ; } return new Proxy (img, { get (obj, prop ) { if (prop === 'src' && !hasLoaded) { return loadingImg; } return Reflect .get (...arguments ); } }); };const img = new Image ();const imgProxy = createImgProxy (img, 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' , 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );document .body .appendChild (imgProxy);
函数节流 这里的函数节流等同于ES5例子中的合并HTTP请求,函数节流的目的是想要控制函数调用的频率,在一段时间内,某个函数只被执行一次。
假设有这样一个简单的函数:
1 2 const handler = ( ) => console .log ('Do something...' );document .addEventListener ('click' , handler);
接着使用ES6 Proxy API创建节流的工厂函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const createThrottleProxy = (fn, rate ) => { let lastClick = Date .now () - rate; return new Proxy (fn, { apply (target, thisArg, args ) { if (Date .now () - lastClick >= rate) { fn (args); lastClick = Date .now (); } } }); };
此时就可以使用函数节流的代理函数,来对指定函数进行节流。
1 2 3 const handler = ( ) => console .log ('Do something...' );const handlerProxy = createThrottleProxy (handler, 1000 );document .addEventListener ('click' , handlerProxy);
使用ES6 Proxy API实现缓存代理——斐波那契数列缓存优化 缓存代理可以将一些开销很大的方法的运算结果进行缓存,再次调用该函数时,若参数一致,则可以直接返回缓存中的结果,而不用再重新进行运算。
假设有这样一个未经优化的斐波那契数列的计算函数。
1 2 3 4 5 6 7 8 const getFib = (number ) => { if (number <= 2 ) { return 1 ; } else { return getFib (number - 1 ) + getFib (number - 2 ); } }
利用代理模式和ES6的Proxy API,可以创建这样一个缓存代理的工厂函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const getCacheProxy = (fn, cache = new Map () ) => { return new Proxy (fn, { apply (target, thisArg, args ) { const argsString = args.join (' ' ); if (cache.has (argsString)) { console .log (`输出${args} 的缓存结果: ${cache.get(argsString)} ` ); return cache.get (argsString); } const result = fn (...args); cache.set (argsString, result); return result; } }) }
此时带有缓存代理的斐波那契数列的计算函数,就可以这样使用:
1 2 3 const getFibProxy = getCacheProxy (getFib);getFibProxy (40 ); getFibProxy (40 );
使用ES6 Proxy API实现简单的表单验证器 这是一个很简单的需求,假设我们有这样一个表单对象和对应的验证规则,我们除了使用之前了解的策略模式,还可以使用代理模式来实现表单校验的需求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const userForm = { account : '' , password : '' , }const validators = { account (value ) { const re = /^[\u4e00-\u9fa5]+$/ ; return { valid : re.test (value), error : '"account" is only allowed to be Chinese' } }, password (value ) { return { valid : value.length >= 6 , error : '"password "should more than 6 character' } } }
利用代理模式和ES6的Proxy API,可以创建这样一个表单验证代理的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const getValidateProxy = (target, validators ) => { return new Proxy (target, { let _validators : validators, set (target, prop, value ) { if (value === '' ) { console .error (`"${prop} " is not allowed to be empty` ); return target[prop] = false ; } const validResult = this ._validators [prop](value); if (validResult.valid ) { return Reflect .set (target, prop, value); } else { console .error (`${validResult.error} ` ); return target[prop] = false ; } } }) }
此时就可以这样使用,完成基本的表单校验需求:
1 2 3 const userFormProxy = getValidateProxy (userForm, validators); userFormProxy.account = '123' ; userFormProxy.password = 'he' ;
使用ES6 Proxy API实现私有属性 在以前,实现JavaScript的私有属性,是很困难的,不过Public and Private Instance Fields Proposal
的提案已经进入了Stage 3
阶段,表示我们以后可以使用#
的语法来表示私有属性和方法。
1 2 3 4 5 6 7 8 9 10 11 class User { #id = 'xyz' ; constructor (name ) { this .name = name; } getUserId ( ) { return this .#id; } }1
但是研究没用私有属性语法之前的私有属性实现,还是很有学习意义的,其中一种做法就是使用ES6 Proxy API劫持相关属性,阻止其返回私有属性。
创建一个这样的私有属性劫持函数,在这里例子中,以_
开头的属性会被认为是私有属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 function getPrivateProps (obj, filterFunc ) { return new Proxy (obj, { get (obj, prop ) { if (!filterFunc (prop)) { let value = Reflect .get (obj, prop); if (typeof value === 'function' ) { value = value.bind (obj); } return value; } }, set (obj, prop, value ) { if (filterFunc (prop)) { throw new TypeError (`Can't set property "${prop} "` ); } return Reflect .set (obj, prop, value); }, has (obj, prop ) { return filterFunc (prop) ? false : Reflect .has (obj, prop); }, ownKeys (obj ) { return Reflect .ownKeys (obj).filter (prop => !filterFunc (prop)); }, getOwnPropertyDescriptor (obj, prop ) { return filterFunc (prop) ? undefined : Reflect .getOwnPropertyDescriptor (obj, prop); } }); }function propFilter (prop ) { return prop.indexOf ('_' ) === 0 ; }
此时就可以使用getPrivateProps
,实现私有属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const myObj = { public : 'hello' , _private : 'secret' , method : function ( ) { console .log (this ._private ); } }, myProxy = getPrivateProps (myObj, propFilter);console .log (JSON .stringify (myProxy)); console .log (myProxy._private ); console .log ('_private' in myProxy); console .log (Object .keys (myProxy)); for (let prop in myProxy) { console .log (prop); } myProxy._private = 1 ;
代理模式在实际项目中的应用
拦截器
使用代理模式代理对象的访问的方式,一般又被称为拦截器 。
拦截器的思想在实战中应用非常多,比如我们在项目中经常使用 Axios
的实例来进行 HTTP 的请求,使用拦截器 interceptor
可以提前对 请求前的数据(request
请求)和 服务器返回的数据(response
)进行一些预处理,比如:
request
请求头的设置,和 Cookie 信息的设置;
权限信息的预处理,常见的比如验权操作或者 Token 验证;
数据格式的格式化,比如对组件绑定的 Date
类型的数据在请求前进行一些格式约定好的序列化操作;
空字段的格式预处理,根据后端进行一些过滤操作;
response
的一些通用报错处理,比如使用 Message 控件抛出错误;
除了 HTTP 相关的拦截器之外,还有路由跳转的拦截器,可以进行一些路由跳转的预处理等操作。
前端框架的数据响应式化
现在的很多前端框架或者状态管理框架都使用上面介绍的 Object.defineProperty
和 Proxy
来实现数据的响应式化,比如 Vue,Vue 2.x 使用前者,而 Vue 3.x 则使用后者。
Vue 2.x 中通过 Object.defineProperty
来劫持各个属性的 setter/getter
,在数据变动时,通过发布-订阅模式发布消息给订阅者,触发相应的监听回调,从而实现数据的响应式化,也就是数据到视图的双向绑定。
为什么 Vue 2.x 到 3.x 要从 Object.defineProperty
改用 Proxy
呢,是因为前者的一些局限性,导致的以下缺陷:
无法监听利用索引直接设置数组的一个项,例如:vm.items[indexOfItem] = newValue
,因此Vue2.x需要使用Vue.$set()
解决响应式的问题。
无法监听数组的长度的修改,例如:vm.items.length = newLength
,同样需要使用Vue.$set()
解决响应式的问题。;
无法监听 ES6 的 Set
、WeakSet
、Map
、WeakMap
的变化;
无法监听 Class
类型的数据;
无法监听对象属性的新加或者删除;
除此之外还有性能上的差异,基于这些原因,Vue 3.x 改用 Proxy
来实现数据监听了。当然缺点就是对 IE 用户的不友好,兼容性敏感的场景需要做一些取舍。
缓存代理
在前面斐波那契数列缓存优化 的内容中,斐波那契数列缓存优化 就是使用缓存代理的思想,将复杂计算的结果缓存起来,下次传参一致时直接返回之前缓存的计算结果。
保护代理和虚拟代理
保护代理 :当一个对象可能会收到大量请求时,可以设置保护代理,通过一些条件判断对请求进行过滤;
比如前面例子中小明经过闺蜜给小姐姐送花,闺蜜认可小明不认可其他人就是保护代理。
虚拟代理 :在程序中可以能有一些代价昂贵的操作,此时可以设置虚拟代理,虚拟代理会在适合的时候才执行操作。
比如小明希望闺蜜送给小姐姐的花延迟到小姐姐心情好再购买,或者图片的预加载,甚至是目前主流的前端骨架屏占位技术,都属于虚拟代理的范畴。
正向代理和反向代理
正向代理: 一般的访问流程是客户端直接向目标服务器发送请求并获取内容,使用正向代理后,客户端改为向代理服务器发送请求,并指定目标服务器(原始服务器),然后由代理服务器和原始服务器通信,转交请求并获得的内容,再返回给客户端。正向代理隐藏了真实的客户端,为客户端收发请求,使真实客户端对服务器不可见;
反向代理: 与一般访问流程相比,使用反向代理后,直接收到请求的服务器是代理服务器,然后将请求转发给内部网络上真正进行处理的服务器,得到的结果返回给客户端。反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。
他们之间最大的区别在于,正向代理的对象是客户端 ,反向代理的对象是服务端 ,正向代理隐藏的是用户,反向代理隐藏的是服务器。
正向代理
先搭建一个属于自己的代理服务器
用户发送请求到自己的代理服务器
自己的代理服务器发送请求到服务器
服务器将数据返回到自己的代理服务器
自己的代理服务器再将数据返回给用户
反向代理
用户发送请求到服务器(访问的其实是反向代理服务器,但用户不知道)
反向代理服务器发送请求到真正的服务器
真正的服务器将数据返回给反向代理服务器
反向代理服务器再将数据返回给用户
在实际的情况中,有时候访问github会比较缓慢,甚至无法打开,我们就需要借助离github服务器比较近的服务器做个中转站,方便我们访问GitHub,在这里代理的对象是客户端,github服务器收到的ip地址请求也只是中转站服务器的ip请求,真实的客户端ip被隐藏,所以这里用的是正向代理。
反向代理多用在服务器端,比如它是处理浏览器跨域问题的常用解决方案之一,CDN,网络设备的负载均衡也能见到反向代理的身影,这里被代理的对象是服务端,对于用户来说,他并不知道反向代理服务器背后真实的服务器信息,所以反向dialing隐藏的是服务器。
反向代理的优点与缺点 代理模式的主要优点有:
代理对象在访问者与目标对象之间可以起到中介和保护目标对象 的作用;
代理对象可以扩展目标对象的功能 ;
代理模式能将访问者与目标对象分离,在一定程度上降低了系统的耦合度 ,如果我们希望适度扩展目标对象的一些功能,通过修改代理对象就可以了,符合开放-封闭 原则;
代理模式的缺点主要是增加了系统的复杂度,要斟酌当前场景是不是真的需要引入代理模式。
代理模式与其他模式的区别 很多其他的模式,比如状态模式、策略模式、访问者模式其实也是使用了代理模式。
代理模式与适配器模式 代理模式和适配器模式都为另一个对象提供间接性的访问,他们的区别:
适配器模式: 主要用来解决接口之间不匹配的问题,通常是为所适配的对象提供一个不同的接口;
代理模式: 提供访问目标对象的间接访问,以及对目标对象功能的扩展,一般提供和目标对象一样的接口;
代理模式与装饰者模式 装饰者模式实现上和代理模式类似,都是在访问目标对象之前或者之后执行一些逻辑,但是目的和功能不同:
装饰者模式: 目的是为了方便地给目标对象添加功能,也就是动态地添加功能;
代理模式: 主要目的是控制其他访问者对目标对象的访问;
参考资料 [CUG-GZ]前端知识进阶——代理模式
https://www.yuque.com/cuggz/feplus/hdsvty
前端设计模式之代理模式
https://juejin.cn/post/6844904190947360781
漫画:什么是 “代理模式” ?
https://mp.weixin.qq.com/s/O8_A2Ms9MPKEe9m6Y6t2-g
JavaScript设计模式与开发实践
https://www.ituring.com.cn/book/1632
从ES6重新认识JavaScript设计模式(五): 代理模式和Proxy
https://segmentfault.com/a/1190000015800703
使用 JavaScript 原生的 Proxy 优化应用
https://juejin.cn/post/6844903539974619143