JavaScript设计模式之代理模式

201 阅读4分钟

介绍

代理模式就是在真正使用的函数之间再加入一个函数,使得我们可以间接去访问使用的函数。那么为什么要多此一举在中间加入一个函数呢,我们可以举两个例子。

🌰一: 小明追求一位女生

方式一

  • 直接把花送给她,百分百会被拒绝。

方式二

  • 通过小明和女生都认识的一位中间人送花,因为中间人了解女生,成功率30%。

方式三

  • 继承方式二,不过这位中间人和女生关系更加亲密,她知道女生开心的时候不会拒绝别人,所以小明把花暂存在中间人这里,中间人通过监听女生的心情来决定何时送花,成功率90%。

🌰二: 图片预加载

在平时开发中,我们可能会遇到图片过大或者网络问题导致图片加载缓慢的问题,那么我们就可以通过代理模式来实现图片的预加载。

	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('本地图片');
				img.src = src
			}
		}
	}()
	proxyImage.setSrc("https://imgs.aixifan.com/nqiFgWPe8D-yuUNf2-YjaUfy-Mza6bm-QRF7Bj.png")

在这个例子中我们通过proxyImage间接访问了myImageproxyImage控制了客户对myImage的访问,并且增加了一些额外的操作,比如在真正的图片加载之前,先加载一张本地的loading图片。

代理的意义

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

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

在面向对象的程序设计中,大多情况下,若违反其他原则,一定会违反 开放 - 封闭 原则。如果我们只是从网上获取体积很小的图片,或者5年后我们的网速已经快到可以忽略这样图片大小的限制,那么我们就可以把proxyImage方法删除掉,直接使用myImage即可。这就是代理的作用,代理负责预加载图片,操作完成之后,再把请求重新交给本体myImage

代理和本体接口的一致性

上面说到,有一天我们不需要代理了,可以直接请求本体。其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理和本地是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这有两个好处:

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

代理模式一:虚拟代理

合并HTTP请求

有一个功能描述是点击复选框的时候,把复选框对应的文件上传到服务器,那么在客户频繁点击的时候也会同时发送大量的请求。如果不是对实时性要求特别高,我们可以创建一个请求池,到达固定的时间后统一发送请求池里的函数。

<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>


<script type="text/javascript">
	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');
	//接下来,给这些checkbox 绑定点击事件,并且在点击的同时往另一台服务器同步文件:
	for (var i = 0, c; c = checkbox[i++];) {
		c.onclick = function () {
			if (this.checked === true) {
				proxySynchronousFile(this.id);
			}
		}
	};
</script>

代理模式二:缓存代理

计算乘积

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

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

	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

通过增加缓存代理的方式,mult函数可以专注于自身的职责-计算乘积,缓存的功能则有代理来实现。

用高阶函数动态创建缓存代理

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

	/**************** 计算乘积 *****************/
	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中最常用的就是虚拟代理和缓存代理,虽然代理模式非常有用,但我们再编写业务时,不需要去猜测是否需要使用代理模式,只有当真正发现不方便直接访问某个对象时,再编写代理也不迟。

参考

JavaScript设计模式与开发实践