《javascript设计模式与开发实践》阅读笔记(6)—— 代理模式

代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
基本可以理解为粉丝(客户),经纪人(代理),偶像(对象)。经纪人就相当于偶像的代理,需求直接提给经纪人,经纪人这边可以进行很多逻辑上的处理,比如可以帮助偶像过滤掉很多请求等等。

1.保护代理和虚拟代理

像上面那种,请求被代理拒绝掉就是保护代理。
把一些开销很大的对象,延迟到真正需要它的时候才去创建的代理,就是虚拟代理。
保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。

2.虚拟代理实现图片预加载

在Web 开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。

 

先随便创建一个img对象

 1     var myImage = (function(){
 2         var imgNode = document.createElement( 'img' );    //创建节点
 3         document.body.appendChild( imgNode );
 4         return {   //闭包返回一个对象,包含可以设置节点src属性的方法
 5             setSrc: function( src ){
 6                 imgNode.src = src;
 7             }
 8         }
 9     })();
10 
11     myImage.setSrc( '真正的图片.jpg' );

这样的代码在网速很慢时,图片位置会出现较长时间空白。现在开始引入代理对象proxyImage,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位图loading.gif,来提示用户图片正在加载。

 1     var myImage = (function(){
 2         var imgNode = document.createElement( 'img' );   //同上
 3         document.body.appendChild( imgNode );
 4         return {           
 5             setSrc: function( src ){
 6                 imgNode.src = src;
 7             }
 8         }
 9     })();
10 
11     var proxyImage = (function(){
12         var img = new Image;         //创建一个图片对象
13         img.onload = function(){     //图片对象异步加载完成后,加载完成包含图片的加载完成,这句代码可以理解为一个监控
14             myImage.setSrc( this.src );  //把节点的src设置成图片对象的
15         }
16         return {     
17             setSrc: function( src ){
18                 myImage.setSrc( 'loading.gif' );   //预先给个占位图片
19                 img.src = src;       //给图片对象设置src属性
20             } 
21         }
22     })();
23 
24     proxyImage.setSrc( '真正的图片.jpg' );  //调用这个方法,会先给目标节点一个占位图片,同时内置的图片对象开始加载图片,当加载完成后,触发设置,节点显示正确图片

这里我们也可以看出,资源的加载只需要一次,只要这张图片下载好了,通过src都能很快显示它。

3.代理的意义

图片预加载功能不通过代理也可以实现,如下:

 1     var MyImage = (function(){
 2         var imgNode = document.createElement( 'img' );  //创建节点
 3         document.body.appendChild( imgNode );
 4         var img = new Image;    //创建一个图片对象
 5         img.onload = function(){  //图片对象异步加载
 6             imgNode.src = img.src;   //节点更换src
 7         };
 8         return {
 9             setSrc: function( src ){
10                 imgNode.src = 'loading.gif';   //预先给个占位图片
11                 img.src = src;    //图片对象设置src
12             }
13         }
14     })();
15 
16     MyImage.setSrc( '真正的图片.jpg' );

看起来没什么问题,但是我们得提到一个面向对象设计的原则——单一职责原则。
一个类(通常也包括对象和函数等),应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏,我们可能不得不繁琐的修改代码或者重写函数。
比如上段代码中的MyImage对象除了负责给img节点设置src外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
我们需要的只是给img 节点设置src,预加载图片只是一个锦上添花的功能。

使用代理模式的代码中,我们并没有改变或者增加MyImage的接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放—封闭原则的。给img节点设置src和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。

4.代理和本体接口的一致性

如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了setSrc 方法。这样做有两个好处,(1)使用者不会被api搞糊涂(2)在任何使用本体的地方都可以替换成使用代理。

特别:如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都能被执行,则可以认为它们也具有一致的“接口”。

 1     var myImage = (function(){
 2         var imgNode = document.createElement( 'img' );
 3         document.body.appendChild( imgNode );
 4         return function( src ){
 5             imgNode.src = src;
 6         }
 7     })();
 8 
 9     var proxyImage = (function(){
10         var img = new Image;
11         img.onload = function(){
12             myImage( this.src );
13         }
14         return function( src ){
15             myImage( 'loading.gif' );
16             img.src = src;
17         }
18     })();
19 
20     proxyImage( '真正的图片.jpg' );

5.虚拟代理合并HTTP请求

前面高阶函数的时候提过节流函数,这里也是一样,为了避免可能频繁触发的请求导致的服务器压力,可以设置一个时间延迟,比如说2s,2s后会把请求一起提交一次。只不过现在这里通过代理来实施。

6.缓存代理

缓存代理的例子——计算乘积

 1     var mult = function(){    //计算乘积的函数
 2         console.log( '开始计算乘积' );
 3         var a = 1;
 4         for ( var i = 0, l = arguments.length; i < l; i++ ){  //遍历参数,计算结果
 5             a = a * arguments[i];
 6         }
 7         return a;   //返回结果
 8     };
 9 
10     mult( 2, 3 ); // 输出:6
11     mult( 2, 3, 4 ); // 输出:24
12 
13     var proxyMult = (function(){
14         var cache = {};   //利用闭包  保留结果,作为缓存
15         return function(){
16             var args = Array.prototype.join.call( arguments, ',' ); //把参数变成字符串
17             if ( args in cache ){   //如果这个字符串名称在缓存中存在
18                 return cache[ args ];    //返回结果
19             }
20             return cache[ args ] = mult.apply( this, arguments );   //不存在,就调用乘积函数计算一次,保存结果
21         }
22     })();
23 
24     proxyMult( 1, 2, 3, 4 ); // 输出:24
25     proxyMult( 1, 2, 3, 4 ); // 输出:24   第二次调用,没有再次计算,而是拿的缓存中的结果

用高阶函数动态创建代理

通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。

 1     /**************** 计算乘积 *****************/
 2     var mult = function(){
 3         var a = 1;
 4         for ( var i = 0, l = arguments.length; i < l; i++ ){
 5             a = a * arguments[i];
 6         }
 7         return a;
 8     };
 9     /**************** 计算加和 *****************/
10     var plus = function(){
11         var a = 0;
12         for ( var i = 0, l = arguments.length; i < l; i++ ){
13             a = a + arguments[i];
14         }
15         return a;
16     };
17     /**************** 创建缓存代理的工厂 *****************/
18     var createProxyFactory = function( fn ){  //这里只是把最后调用的函数变成参数传进来而已
19         var cache = {};
20         return function(){
21             var args = Array.prototype.join.call( arguments, ',' );
22             if ( args in cache ){
23                 return cache[ args ];
24             }
25             return cache[ args ] = fn.apply( this, arguments );
26         }
27     };
28 
29     var proxyMult = createProxyFactory( mult ),
30     proxyPlus = createProxyFactory( plus );
31     alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
32     alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
33     alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
34     alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10

 

总结

代理模式包括许多小分类,在JavaScript 开发中最常用的是虚拟代理和缓存代理。代理模式可以理解为一个中转站,其内部必然会对本体进行调用。我们在编写业务代码的时候,不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。

原文地址:https://www.cnblogs.com/grey-zhou/p/6088157.html