Aelous-Blog

养一猫一狗,猫叫夜宵,狗叫宵夜

0%

JS设计模式-代理模式

代理模式(Proxy)

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

代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出做出一些处理之后,再把请求转交给本体对象。

小明追 MM

小明追女孩 A,因为内向,所以找了他和 A 的共同好友 B,帮助小明完成送花的壮举。首先看看不实用代理的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Flower = function() {};

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
};

var A = {
receiveFlower: function(flower) {
console.log("收到花" + flower);
}
};

xiaoming.sendFlower(A);

接下来我们引入代理 B,小明通过 B 来给 A 送花:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Flower = function() {};

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
};

var B = {
receiveFlower: function(flower) {
A.receiveFlower(flower);
}
};

var A = {
receiveFlower: function(flower) {
console.log("收到花" + flower);
}
};

xiaoming.sendFlower(B);

显然第二段结果和第一段一样,这是最简单的代理模式的编写。从这段代码看起来,代理模式除了把事情变得复杂没啥用了。但是如果 A 在不同的情况下接收到花会有不同的结果,如果小明送花的时候不合时宜,那就不会有正确的结果了。但是 B 很了解 A,她知道 A 什么时候心情好,所以这时候代理的用处就来了。

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
var Flower = function() {};

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
};

var B = {
receiveFlower: function(flower) {
A.listenGooodMood(function() {
// 监听A的状态
A.receiveFlower(flower);
});
}
};

var A = {
receiveFlower: function(flower) {
console.log("收到花" + flower);
}
listenGoodMood:function(fn){
setTimeout(function(){
// 假定10秒后心情变好
fn();
},10000);
}
};

xiaoming.sendFlower(B);

预祝小明求爱成功。

保护代理和虚拟代理

虽然这是一个虚拟案例,但是我们从中可以看到两种代理模式的身影。代理 B 可以帮助 A 过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理 B 处被拒绝掉。这种代理就叫做保护代理

另外,在程序世界中,new Flower 是一个代价昂贵的操作,我们可以把 new Flower 的操作交给代理 B 去执行,代理 B 会在 A 心情好的时候去 new Flower,这是代理的另一种模式,称为虚拟代理。虚拟代理会把一些开销很大的对象,延迟到真正需要它的时候才去创建。代码如下:

1
2
3
4
5
6
7
8
var B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
var flower = new Flower();
A.receiveFlower(flower);
});
}
};

保护代理用于控制不同权限的对象对目标对象的访问,但在 JavaScript 并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。

接下来我们看一个真实案例。

虚拟代理实现图片预加载

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

下面我们来实现这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建一个 img 标签,并且提供一个对外的 setSrc 接口,外界调用这个接口,便可以给该 img 标签设置 src 属性:

1
2
3
4
5
6
7
8
9
10
11
12
var myImage = (function() {
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);

return {
setSrc: function(src) {
imgNode.src = src;
}
};
})();

myImage.setSrc("http://x/x/x.jpg");

现在引入代理对象 proxyImage,通过这个代理对象,在图片真正被加载好之前,页面中将出现一张占位图,来提示用户图片正在加载。代码如下:

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
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://x/x/x.jpg");
img.src = src;
}
};
})();

proxyImage.setSrc("http://x/x/x.jpg");

我们通过 proxyImage 间接的访问 MyImage。proxyImage 控制了客户对 MyImage 的访问,并且在此过程中加入一些额外操作,比如在图片加载好之前,先把 img 的节点设置为一张本地图片。

代理的意义

不用代理的预加载图片函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var MyImage = (function() {
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
var img = new Image();

img.onload = function() {
imgNode.src = img.src;
};

return {
setSrc: function(src) {
imgNode.src = "file://x/x/x.jpg";
img.src = src;
}
};
})();

MyImage.setSrc("http://x/x/x.jpg");

为了说明代理的意义,我们引入一个面向对象设计的原则——单一职责原则

单一职责原则指的是:就一个类(通常也包括对象和函数等)而言,应当仅有一个引起它变化的原因。

如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

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

代理和本体接口的一致性

如果我们有一天不需要预加载功能了,我们可以直接将目标指向本体。在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样有两个好处:

  • 用户可以放心的请求代理,他只关心是否能得到想要的结果。
  • 在任何使用本体的地方都可以替换成使用代理。

在 JAVA 中,代理和本体都要显式的实现同一个接口才行,一方面接口保证了它们会拥有同样的方法,另一方面,面向接口编程迎合依赖倒置原则,通过接口进行向上转型,从而避开编译器的类型检查,代理和本体将来可以被替换使用。

但是在 JS 这类动态语言中,我们有时通过鸭子类型检测代理和本体是否都实现了 setSrc 方法,另外大多数时候干脆不做检查,全靠自觉。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.setSrc(this.src);
};
return function(src) {
myImage.setSrc("file://x/x/x.jpg");
img.src = src;
};
})();

proxyImage("http://x/x/x.jpg");

虚拟代理合并 HTTP 请求

先想象这样一个场景:每周我们都要写一份工作周报,周报要交给总监,总监手下管理着 150 个员工,如果我们每个人都直接把周报发给总监,总监估计要 GG。

现在我们把周报发给组长,组长作为代理,把内容提炼后一次性发给总监。

这个例子在程序世界里很容易引起共鸣,在 Web 开发中,也许最大的开销就是网络请求。假设我们在做一个文件同步的功能,当我们选中 checkbox 的时候,它对应的文件就会被同步到另一台备用服务器上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
var syncronousFile = 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 秒的延迟不会有太大副作用,却能大大减少服务器压力。代码如下:

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
var syncronousFile = 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() {
syncronousFile(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);
}
};
}

虚拟代理在惰性加载中的应用

惰性加载 emmm 就是延迟加载,或者说在需要的情况下进行加载,例如:无限滚动、瀑布流,这是一种设计模式。

缓存代理

缓存代理可以为一些开销打的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数和之前的一致,则可以直接返回前面存储的运算结果。

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

先创建一个用于求乘积的函数:

1
2
3
4
5
6
7
8
9
10
11
var mult = function() {
console.log("开始计算乘积");
var a = 1;
for (var i = 0, l = arguments.length; i < 1; i++) {
a = a * arguments[i];
}
return a;
};

mult(2, 3); // 6
mult(2, 3, 4); // 24

现在加入缓存代理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

当我们第二次调用 proxyMult(1,2,3,4)的时候,本体 mult 函数并没有被计算,proxyMult 直接返回了之前缓存好的计算结果。

通过增加缓存代理的方式,mult 函数可以继续专注于自身的职责——计算乘积,缓存的功能是由代理对象实现的。

缓存代理用于 ajax 异步请求数据

我们常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。

显然这里也可以引入缓存代理,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。

用高阶函数动态创建代理

通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。现在这些计算方法被当作参数传入一个专门用于创建缓存代理的工厂中,这样一来,我们就可以为乘法、加法、减法等创建缓存代理,代码如下:

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
/********** 计算乘积 **********/
var mult = function() {
var a = 1;
for (var i = 0, l = arguments.length; i < 1; i++) {
a = a * arguments[i];
}
return a;
};

/********** 计算加和 **********/
var plus = function() {
var a = 0;
for (var i = 0, l = arguments.length; i < 1; 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

其他代理模式

代理模式的变体种类非常多,限于篇幅以及在 JS 的适用性,就只说这些代理,不一一详解。

  • 防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。
  • 远程代理:为一个对象在不同的地址空间提供局部代表,在 JAVA 中,远程代理可以是另一个虚拟机重的对象。
  • 保护代理:用于对象应该有不同访问权限的情况。
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
  • 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运动场景。

小结

代理模式包括许多小分类,在 JavaScript 开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常游泳,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。

End~~ 撒花= ̄ω ̄=花撒
如果您读文章后有收获,可以打赏我喝咖啡哦~