代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
一、保护代理和虚拟代理
保护代理:代理 B 可以帮助 A 过滤掉一些请求,这种请求就可以直接在代理 B处被拒绝掉。
虚拟代理:把一些开销很大的对象,延迟到 真正需要它的时候才去创建。
保护代理用于控制不同权限的对象对目标对象的访问,但在 JavaScript 并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。
二、代理的意义
在 Web 开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。
不用代理的预加载图片函数
var myImage = (function(){ //imgNode为实际的DOM节点 var imgNode = document.createElement('img'); document.body.appendChild(imgNode); //img用于预加载所需图片 var img = new Image(); //img加载完毕后修改img节点的url img.onload = function(){ imgNode.src = img.src; } //imgNode节点的url为占位图 return { setSrc: function(src) { imgNode.src = '...'; img.src = src; } } })() myImage.setSrc( 'http://www.baidu.com/img/bd_logo1.png' );
单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多, 等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
职责被定义为“引起变化的原因”。上段代码中的 MyImage 对象除了负责给 img 节点设置 src外,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
另外,在面向对象的程序设计中,大多数情况下,若违反其他任何原则,同时将违反开放—封闭原则。如果我们希望把预加载图片的这段代码从 MyImage 对象里删掉。这时候就不得不改动MyImage 对象了。
实际上,我们需要的只是给 img 节点设置 src,预加载图片只是一个锦上添花的功能。于是代理的作用在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体 MyImage 。
纵观整个程序,我们并没有改变或者增加 MyImage 的接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放—封闭原则的。给 img 节点设置 src 和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。
三、虚拟代理实现图片预加载
引入代理对象 proxyImage ,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的图片 loading.gif, 来提示用户图片正在加载。
var myImage = (function(){ var imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode ); return { setSrc: function( src ){ imgNode.src = src; } } })(); var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage.setSrc( this.src ); } return { setSrc: function( src ){ myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' ); img.src = src; } } })(); proxyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
四、代理和本体接口的一致性
上一节说到,如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了 setSrc 方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处:
- 用户可以放心地请求代理,他只关心是否能得到想要的结果。
- 在任何使用本体的地方都可以替换成使用代理。
如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都能被执行,则可以认为它们也具有一致的“接口”:
var myImage = (function(){ var imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode ); return function( src ){ imgNode.src = src; } })(); var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage( this.src ); } return function( src ){ myImage( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' ); img.src = src; } })(); proxyImage( 'http:// imgcache.qq.com/music// N/k/000GGDys0yA0Nk.jpg' );
五、虚拟代理合并 HTTP 请求
假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同步到另外一台备用服务器上面。
<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>
接下来,给这些 checkbox 绑定点击事件,并且在点击的同时往另一台服务器同步文件:
var synchronousFile = function( id ){ console.log( '开始同步文件,id 为: ' + id ); }; var checkbox = document.getElementsByTagName( 'input' ); for ( var i = 0, c; c = checkbox[ i++ ]; ){ c.onclick = function(){ if ( this.checked === true ){ synchronousFile( this.id ); } } };
如果在短时间内点击多个checkbox,会往服务器发送多个同步文件请求,如此频繁的网络请求将会带来相当大的开销。
解决方案是,我们可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统,2 秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。
var synchronousFile = function( id ){ console.log( '开始同步文件,id 为: ' + id ); }; var proxySynchronousFile = (function(){ var cache = [], // 保存一段时间内需要同步的ID timer; // 定时器 return function( id ){ cache.push( id ); if ( timer ){ // 保证不会覆盖已经启动的定时器 return; } timer = setTimeout(function(){ synchronousFile( cache.join( ',' ) ); // 2 秒后向本体发送需要同步的ID 集合 clearTimeout( timer ); // 清空定时器 timer = null; cache.length = 0; // 清空ID 集合 }, 2000 ); } })(); var checkbox = document.getElementsByTagName( 'input' ); for ( var i = 0, c; c = checkbox[ i++ ]; ){ c.onclick = function(){ if ( this.checked === true ){ proxySynchronousFile( this.id ); } } };
六、虚拟代理在惰性加载中的应用
我们项让浏览器有必要的时候才开始加载miniConsole.js文件,比如当用户按下 F2 来主动唤出控制台的时候。在 miniConsole.js 加载之前,为了能够让用户正常地使用里面的 AP,通常我们的解决方案是用一个占位的 miniConsole 代理对象来给用户提前使用,这个代理对象提供给用户的接口,跟实际的 miniConsole 是一样的。
我们可以把打印 log 的请求都包裹在一个函数里面,这个包装了请求的函数就相当于其他语言中命令模式中的 Command 对象。随后这些函数将全部被放到缓存队列中,这些逻辑都是在 miniConsole 代理对象中完成实现的。等用户按下 F2 唤出控制台的时候,才开始加载真正的miniConsole.js 的代码,加载完成之后将遍历 miniConsole 代理对象中的缓存函数队列,同时依次执行它们。
//未加载真正的 miniConsole.js 之前的代码如下: var cache = []; var miniConsole = { log: function(){ var args = arguments; cache.push( function(){ return miniConsole.log.apply( miniConsole, args ); }); } }; miniConsole.log(1); //当用户按下 F2 时,开始加载真正的 miniConsole.js,代码如下: var handler = function( ev ){ if ( ev.keyCode === 113 ){ var script = document.createElement( 'script' ); script.onload = function(){ for ( var i = 0, fn; fn = cache[ i++ ]; ){ fn(); } }; script.src = 'miniConsole.js'; document.getElementsByTagName( 'head' )[0].appendChild( script ); } }; document.body.addEventListener( 'keydown', handler, false ); // miniConsole.js 代码: miniConsole = { log: function(){ // 真正代码略 console.log( Array.prototype.join.call( arguments ) ); } };
要保证在 F2 被重复按下的时候,miniConsole.js 只被加载一次。整理一下 miniConsole 代理对象的代码,使它成为一个标准的虚拟代理对象:
var miniConsole = (function(){ var cache = []; var handler = function( ev ){ if ( ev.keyCode === 113 ){ var script = document.createElement( 'script' ); script.onload = function(){ for ( var i = 0, fn; fn = cache[ i++ ]; ){ fn(); } }; script.src = 'miniConsole.js'; document.getElementsByTagName( 'head' )[0].appendChild( script ); document.body.removeEventListener( 'keydown', handler );// 只加载一次miniConsole.js } }; document.body.addEventListener( 'keydown', handler, false ); return { log: function(){ var args = arguments; cache.push( function(){ return miniConsole.log.apply( miniConsole, args ); }); } } })(); miniConsole.log( 11 ); // 开始打印log // miniConsole.js 代码 miniConsole = { log: function(){ // 真正代码略 console.log( Array.prototype.join.call( arguments ) ); } }
七、缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
7.1 计算乘积
//先创建一个用于求乘积的函数: var mult = function(){ console.log( '开始计算乘积' ); var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return a; }; mult( 2, 3 ); // 输出:6 mult( 2, 3, 4 ); // 输出:24
//现在加入缓存代理函数: var proxyMult = (function(){ var cache = {}; return function(){ var args = Array.prototype.join.call( arguments, ',' ); if ( args in cache ){ return cache[ args ]; } return cache[ args ] = mult.apply( this, arguments ); } })(); proxyMult( 1, 2, 3, 4 ); // 输出:24 proxyMult( 1, 2, 3, 4 ); // 输出:24
7.2 缓存代理用于ajax异步请求数据
我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。显然这里也可以引入缓存代理,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。
八、用高阶函数动态创建代理
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。现在这些计算方法被当作参数传入一个专门用于创建缓存代理的工厂中, 这样一来,我们就可以为乘法、加法、减法等创建缓存代理:
/**************** 计算乘积 *****************/ var mult = function(){ var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return a; }; /**************** 计算加和 *****************/ var plus = function(){ var a = 0; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a + arguments[i]; } return a; }; /**************** 创建缓存代理的工厂 *****************/ var createProxyFactory = function( fn ){ var cache = {}; return function(){ var args = Array.prototype.join.call( arguments, ',' ); if ( args in cache ){ return cache[ args ]; } return cache[ args ] = fn.apply( this, arguments ); } }; var proxyMult = createProxyFactory( mult ), proxyPlus = createProxyFactory( plus ); alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24 alert ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10