一. 代理模式的定义
代理模式的定义:为其他对象提供一种代理,以控制对着这个对象的访问。
在代理模式中,一个对象充当另一个对象的接口。
这种模式看起来像是额外的开销,但是出于性能因素的考虑却是非常有用的。代理充当了本体对象的守护对象,并且试图使本体对象做尽可能少的工作。
二. 代理模式的适用场景
代理模式的适用场景有:
延迟一个大对象的实例化;
访问远程对象;
访问控制;
… …
三. 代理模式的实现
在代理模式中,一个对象充当另一个对象的接口,使得本体对象做尽可能少的工作。
/* =============== 本体类 =============== */
var Client = function() {};
Client.prototype = {
add: function() {
// 添加功能... ...
},
delete: function() {
// 删除功能... ...
},
update: function() {
// 修改功能... ...
}
};
/* =============== 代理类 =============== */
var Proxy = function() {
this.client = new Client();
};
Proxy.prototype = {
add: function() {
return this.client.add();
},
delete: function() {
return this.client.delete();
},
update: function() {
return this.client.update();
}
};
3.1 虚拟代理
假如Client类有很多方法,并且大多数都庞大且复杂,为了实例它会占用很多很多CPU。那当我们需要使用这个对象时才去实例化它不是更好吗?虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
我们把上面的代码用虚拟代理重构一下:
/* =============== 本体类 =============== */
var Client = function() {};
Client.prototype = {
add: function() {
// 添加功能... ...
},
delete: function() {
// 删除功能... ...
},
update: function() {
// 修改功能... ...
}
};
/* =============== 代理类 =============== */
var Proxy = function() {
this.client = null;
};
Proxy.prototype = {
// 在必要的时候才创建实例对象
_init: function() {
if (!this.client) {
this.client = new Client();
}
},
add: function() {
this._init();
return this.client.add();
},
delete: function() {
this._init();
return this.client.delete();
},
update: function() {
this._init();
return this.client.update();
}
};
3.2 缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储。在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
例如:
/* =============== 开销大的本体类 =============== */
var calculate = function() {
var result;
// 复杂且庞大的计算 .... ...
return result;
};
/* =============== 缓存代理类 =============== */
var calculateProxy = (function() {
var cache = {}; // 缓存计算结果
return function() {
var args = Array.prototype.join.call(arguments, ",");
if(args in cache) {
return cache[args];
}
return cache[args] = calculate.apply(this, arguments);
}
})();
/* =============== 客户端实现 =============== */
calculateProxy(1, 2, 3, 4, 5); // 本体calculate函数被计算,并写入缓存结果
calculateProxy(1, 2, 3, 4, 5); // 本体calculate函数并没有被计算,而是直接返回之前缓存好的计算结果
calculateProxy(1, 2, 3); // 本体calculate函数被计算,并写入缓存结果
通过增加缓存代理的方式,本体calculate函数可以专注于自身的计算职能,而缓存的额功能则由代理对象来实现。
3.3 用高阶函数动态创建代理
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。这些方法被当作参数传入一个专门用于创建缓存代理的工厂中。这样,我们就可以为加减乘除等创建缓存代理,代码如下:
/********** 计算乘积 **********/
var mult = function() {
var result = 1;
for(var i = 0, l = arguments.length; i < l; i++) {
result = result * arguments[i];
}
return result;
};
/********** 计算加和 **********/
var plus = function() {
var result = 0;
for(var i = 0, l = arguments.length; i < l; i++) {
result = result + arguments[i];
}
return result;
};
/********** 创建缓存代理的工厂 **********/
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 multProxy = createProxyFactory(mult);
var plusProxy = createProxyFactory(plus);
/********** 客户端实现 **********/
multProxy(1, 2, 3, 4, 5); // 120
plusProxy(1, 2, 3, 4, 5); // 15
3.4 其他代理模式
代理模式的变种很多,主要有:
远程代理:为一个对象在不同的地址空间提供局部代表。
保护代理:用于控制不同权限的对象对目标对象的访问。
智能引用代理:取代了简单的指针,它在访问对象时执行了一些附加操作,比如计算一个对象被引用的次数。
… …
代理模式包括许多小分类,在JavaScript开发中最常用的是虚拟代理和缓存代理。
四. 代理模式的实际应用
4.1 虚拟代理实现图片预加载
在Web开发中,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张图片作为loading图占位,然后用异步的方式加载图片,等图片加载完成后再将其插入img节点中。这种延迟初始化的场景就很适合使用虚拟代理。
引入代理对象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("loading.gif");
img.src = src;
}
};
})();
proxyImage.setSrc("yun_qi_img/1.jpg");
备注:该代码摘抄自《JavaScript设计模式与开发实践》第6章P92。
4.2 虚拟代理实现合并HTTP请求
假设在做一个标签管理的功能时,当点击标签删除按钮,该对应的标签就会对服务器进行标签删除的网络请求。当在短时间内点击多次标签删除按钮,可以预见,如此频繁的网络请求将会带来相当大的开销。
解决方案是:我们可以收集一段时间内的请求,最后一次性发送给服务器。比如等待2秒钟之后,才把这2秒之内需要删除的标签打包发送给服务器。
/* ============== 删除标签的本体类 ============== */
var deleteTag = function(tagName) {
// 删除标签的网络请求与功能实现
// ... ...
}
/* ============== 删除标签的代理类 ============== */
var deleteTagProxy = (function() {
var cache = [], // 保存一段时间需要删除的标签名
timer; // 定时器
return function(tagName) {
cache.push(tagName);
if(timer) { // 保证不会覆盖已经启动的定时器
return;
}
// 2s后向本体发送需要同步的标签名集合
timer = setTimeout(function() {
deleteTag(cache.join(","));
// 清空定时器
clearTimeout(timer);
timer = null;
// 清空标签名集合
cache = [];
}, 2000);
};
})();
/* ============== 删除标签的交互实现 ============== */
/*
* 标签删除按钮的DOM结构为:<div class="btn-delete-tag" data-tagName="my-tag"></div>
*/
var deleteTagBtn = document.getElementByClassName("btn-delete-tag");
deleteTagBtn.forEach(function(element, index) {
element.addEventListener("click", function() {
deleteTagProxy(this.dataSet.tagName);
}, false);
});
4.3 缓存代理用于ajax异步请求数据
在项目中常常会遇到分页的需求。同一页的数据理论上只需要去后台拉去一次。这些已经拉取好的数据在某个地方被缓存之后,下次再请求同一页时,便可以直接从缓存中读取数据。
这里适合使用缓存代理模式。
/* =============== ajax工具函数 =============== */
function ajax(options) {
options = options || {};
options.type = (options.type || "GET").toUpperCase();
options.dataType = options.dataType || "json";
var params = formatParams(options.data);
//创建XMLHttpRequest
if (window.XMLHttpRequest) { // IE6+及现代浏览器
var xhr = new XMLHttpRequest();
} else { //IE6及其以下版本浏览器
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
// 接收数据
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
var status = xhr.status;
if (status >= 200 && status < 300) {
options.success && options.success(xhr.responseText, xhr.responseXML);
} else {
options.fail && options.fail(status);
}
}
}
// 连接和发送数据
if (options.type == "GET") {
xhr.open("GET", options.url + "?" + params, true);
xhr.send(null);
} else if (options.type == "POST") {
xhr.open("POST", options.url, true);
xhr.send(params);
}
}
//格式化参数
function formatParams(data) {
var arr = [];
for (var name in data) {
arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
}
arr.push(("v=" + Math.random()).replace(".",""));
return arr.join("&");
}
/* =============== ajax异步请求分页数据 =============== */
var getPageContext = function(pageId) {
var result;
ajax({
url: "/test/index.php",
method: "get",
dataType: "json",
data: {
id: pageId
},
success: function(response) {
result = response;
}
});
return result;
};
/* =============== 缓存代理类 =============== */
var getPageContextProxy = (function() {
var cache = {}; // 缓存计算结果
return function() {
var args = Array.prototype.join.call(arguments, ",");
if(args in cache) {
return cache[args];
}
return cache[args] = getPageContext.apply(this, arguments);
}
})();
/* =============== 客户端实现 =============== */
getPageContextProxy(1); // 向服务器请求第1页数据
getPageContextProxy(2); // 向服务器请求第2页数据
getPageContextProxy(1); // 从缓存中读取第1页数据
五. 总结
在JavaScript开发中最常用的是虚拟代理和缓存代理。
虽然代理模式很有用,但是在实际业务开发中,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象时,再编写代理也不迟。