概念
代理模式是为一个对象提供一个代用品或占位符。 我理解的代理模式。类似于经纪人,如果想请一个明星办一场商业活动,只能通过经纪人进行沟通联系,经纪人把商业演出的细节和报酬谈好后,再把合同拿给明星签名。
代理模式的应用
下面来看日常中如何从实际情况出发来使用代理模式。
1. 图片预加载
在 Web 开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性, 由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白,常见的做法是先用一张 loading 图片占位。这种场景就适合使用代理模式。
常规的实现图片预加载
常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里。
实现图片预加载如下代码:
var MyImage = function (src) {
// dom节点设置、挂在body上
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
// 先显示loading占位
imgNode.src = 'XXXX/loading.gif';
// 负责预加载图片
const img = new image();
img.onload= function () {
imgNode.src = src
}
img.src = src;
}
MyImage('http://XXXXX/info.jpg')
上段代码中的 MyImage 对象除了负责给 img 节点设置 src外,还要负责显示loading ,还要负责预加载图片。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
为了说明代理的意义,引入一个面向对象设计的原则——单一职责原则:
单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。
我理解就是一个函数就干一件事,别把业务都放在一个函数逻辑里。要不然耦合度太高
如果我们只是从网络上获取一些体积很小的图片,或者 5 年后的网速快到根本不再需要预加载,我们可能希望把预加载图片的这段代码从 MyImage 对象里删掉。这时候就不得不改动MyImage 对象了。
实际上,我们需要的只是给 img 节点设置 src,预加载图片只是一个锦上添花的功能。
所以进而优化代码。
代理模式实现图片预加载
拆分功能(1.给 img 节点设置 src)和(2. 预加载图片)
用代理模式来重新写一下:
var MyImage = function (src) {
// 给 img 节点设置 src
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
imgNode.src = src;
return imgNode;
}
// proxyImage为代理函数
var proxyImage = function (src) {
// 加载loading
let imgNode = MyImage('XXXX/loading.gif');
// 预加载图片
var img = new Image;
img.onload = function () {
imgNode.src = src;
};
img.src = src;
}
proxyImage('http://XXXXX/ys0yA0Nk.jpg')
现在我们通过 proxyImage 间接地访问 MyImage。proxyImage 控制了客户对 MyImage 的访问,并 且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置为 一张本地的 loading 图片。但是这仅仅是代理模式的壳子套上了 具体还有要有两点注意
-
- MyImage的函数接口不明确
-
- 没有保证代理要保和本体接口的一致性
1. 避免函数接口不明确
/**
错误示例 :
var MyImage = function (src) {
// 给 img 节点设置 src
var imgNode = document.createElement( 'img' );
....
return imgNode;
}
//返回的 imgNode 调用者可以随意更改 并没有约束,函数唯一性不明确,比如调用者可以使用
// imgNode.width = XXX 甚至将 imgNode = null 这样就会影响到后续其他调用者
**/
// MyImage函数 应该返回 一个 setStr 接口,setStr 来提供更改src
// 而不是返回img对象 把问题抛给调用者 让自己去img对象上找相关属性
var MyImage = (function () {
// 给 img 节点设置 src
var imgNode = document.createElement( 'img' );
....
return {
// 提供修改img接口
setStr: function(src) {
imgNode.src = src;
}
};
})()
// 如上 MyImage函数 改为立即执行函数 ,使其调用方式为 MyImage.setStr(),
// 如此既封装了内部又提供了对外的setStr接口 让调用者使用,
// 更加规范了函数用法和调用者使用规范。
var proxyImage = function (src) {
MyImage.setStr('XXXX/loading.gif');
var img = new Image;
img.onload = function () {
MyImage.setStr(src);
};
img.src = src;
}
proxyImage('http://XXXXX/ys0yA0Nk.jpg')
2. 代理对象和本体接口的一致性
如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是本体都对外提供了 setSrc 方法,代理对象理应提供 setSrc 方法。
这样做有两个好处。
- 用户可以放心地请求代理,他只关心是否能得到想要的结果。
- 在任何使用本体的地方都可以替换成使用代理。
var proxyImage = (function (src) {
var img = new Image();
img.onload = function () {
MyImage.setStr(this.src);
};
return {
// 和本体接口保持一致
setStr: function (src) {
MyImage.setStr('XXXX/loading.gif');
img.src = src;
}
}
})()
proxyImage.setSrc('http://XXXXX/ys0yA0Nk.jpg')
另外值得一提的是,如果代理对象和本体对象都为一个函数(函数也是对象),函数必然都能被执行,则可以认为它们也具有一致的“接口”,代码如下:
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( 'XXX/loading.gif' );
img.src = src;
}
})();
proxyImage( 'http://XXXXX/A0Nk.jpg' );
2. 合并 HTTP 请求
在 Web 开发中,也许最大的开销就是网络请求。
假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同 步到另外一台备用服务器上面 我们先在页面中放置好这些 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
</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 );
}
}
};
当我们选中 3 个 checkbox 的时候,依次往服务器发送了 3 次同步文件的请求。解决方案是,我们可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。
比如我们等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统,2 秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。
首先还记得 本体和代理模式要保持一致性?。
synchronousFile 方法是可以直接调用的,所以 proxySynchronousFile 也应该是可以直接调用
代码如下:
var proxySynchronousFile = (function (id) {
let timer,
cache = [] // 保存一段时间内需要同步的 ID
return function (id) {
cache.push(id)
if(timer) return;
timer = setTimeout(function () {
synchronousFile(cache.join(','));
clearTime(timer);
cache.length = 0;
timer = null;
}, 2000)
}
})()
proxySynchronousFile(id)
3. 计算乘积
先创建一个用于求乘积的函数:
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
4. 用于ajax异步请求数据
我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。
显然这里也可以引入代理函数,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。 具体代码不再赘述,可以自行实现。
5. 用高阶函数动态创建代理
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。现在这些计算方法被当作参数传入一个专门用于创建代理的工厂中, 这样一来,我们就可以为乘法、加法、减法等创建缓存代理,代码如下:
/**************** 计算乘积 *****************/
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
总结
代理模式的变体种类非常多,限于篇幅及其在 JavaScript 中的适用性,就不一一详细展开说明了。
最重要的是 有一天我们不再需要代理对象,只需要改成代理函数名字改为本体对象, 除此之外用户不需要再做多余的操作,实现了这样调用机制才算是合格的代理模式。