Skip to main content

第六章 代理模式

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

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

6.2 保护代理和虚拟代理

  • 保护代理:保护代理用于控制不同权限的对象对原始对象的访问(帮助原始对象过滤掉一些访问和请求)。

  • 虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建。

6.3 虚拟代理实现图片预加载

在 Web 开发中,图片预加载是一种常用的技术。

如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。

常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。

在一个网络资源的加载可能会消耗很多时间和资源的情况下,也很适合使用虚拟代理。

这种方式可以优化性能,特别是在初始化开销较大或者资源有限时。

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();

// 当真实 URL 的图像加载完成,onload 函数会被调用
// 将真实的图像 URL(已经成功加载的URL)设置给 myImage 对象,替换掉菊花图。
img.onload = function () {
myImage.setSrc(this.src);
};
return {
setSrc: function (src) {
myImage.setSrc("file:// /C:/Users/svenzeng/Desktop/loading.gif");
// 给虚拟图像对象 img 设置真实的 URL,这会触发图像加载
img.src = src;
},
};
})();

proxyImage.setSrc("http://imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg");

6.4 代理的意义

代理的使用,让职责的划分更加清晰,让代码更符合开放-封闭原则单一职责原则,提高了代码的可维护性。

6.5 代理和本体接口的一致性

从接口的使用者来看,代理和本体接口是一致的。他们并不知道代理和本体接口的区别。

6.6 虚拟代理合并 HTTP 请求

假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同步到另外一台备用服务器上面。

如果我们一秒钟内点击好几个,那这样频繁的网络请求将会带来相当大的开销。

这种时候,就可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。

比如我们等待 2 秒钟,之后再把 2 秒钟之内的请求打包发给服务器。

如果不是实时性要求非常高的系统,2 秒的延迟不会带来太大副作用。

var synchronousFile = function (id) {
console.log("开始同步文件,id为: "s + 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);
}
};
}

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

miniConsole.log 这个控制台可以帮助开发者在 IE 浏览器以及移动端浏览器上进行一些简单的调试工作。

miniConsole.js 的代码量大概有 1000 行左右,也许我们并不想一开始就加载这么大的 JS 文件,因为也许并不是每个用户都需要打印 log。

我们希望在有必要的时候才开始加载它,比如当用户按下 F2 来主动唤出控制台的时候。

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));
},
};

6.8 缓存代理

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

6.8.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;
};

var proxyMult = (function () {
var cache = {};
return function () {
var args = Array.prototype.join.call(arguments, ", ");
if (args in cache) {
console.log(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 直接返回了之前缓存好的计算结果。

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

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

请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。

6.9 用高阶函数动态创建代理

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

/**************** 计算乘积 *****************/

var mult = function () {
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
console.log("计算乘积");
return a;
};

/**************** 计算加和 *****************/

var plus = function () {
var a = 0;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a + arguments[i];
}
console.log("计算加和");
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

6.10 其他代理模式

代理模式的变体种类还有非常多。比如:

  1. 防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。

  2. 远程代理:为一个对象在不同的地址空间提供局部代表,在 Java 中,远程代理可以是另一个虚拟机中的对象。

  3. 保护代理:用于对象应该有不同访问权限的情况。

  4. 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。

  5. 写时复制代理:通常用于复制一个庞大对象的情况。 写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。 写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。

6.11 总结

在 JavaScript 中,最常用的是虚拟代理和缓存代理。