本文深入解析JavaScript中的桥接模式,通过事件监控和XHR连接队列两个实例,展示如何将抽象部分与实现部分分离。文章详细讲解了如何构建支持重试、暂停和订阅的异步请求队列,并提供了完整的代码实现与HTML示例,帮助开发者理解桥接模式在实际项目中的应用。
桥接模式概述
本文旨在深入探讨JavaScript中的一种重要设计模式。
桥接模式(Bridge)的核心思想是将抽象部分与其实现部分进行分离,使得两者可以独立地进行变化而互不影响。
正文部分将详细展开这一模式的实际应用。
在JavaScript开发中,桥接模式最典型的应用场景之一是事件监控机制。首先让我们通过一段基础代码来直观感受:
代码如下:
addEvent(element, 'click', getBeerById);
function getBeerById(e) {
var id = this.id;
asyncRequest('GET', 'beer.uri?id=' + id, function(resp) {
// Callback response.
console.log('Requested Beer: ' + resp.responseText);
});
}上述代码存在一个明显的缺陷:getBeerById函数依赖于浏览器的上下文环境,因为它内部使用了this.id属性。如果脱离了特定的上下文环境,该函数将无法正常工作。因此,具备一定经验的开发者通常会对其进行重构,改进后的形式如下:
代码如下:
function getBeerById(id, callback) {
// 通过ID发送请求,然后返回数据
asyncRequest('GET', 'beer.uri?id=' + id, function(resp) {
// callback response
callback(resp.responseText);
});
}这种写法显然更加实用:ID可以作为参数自由传入,同时提供了一个callback函数用于自定义处理逻辑。但这与桥接模式有何关联?接下来的代码将揭示其中的联系:
代码如下:
addEvent(element, 'click', getBeerByIdBridge);
function getBeerByIdBridge (e) {
getBeerById(this.id, function(beer) {
console.log('Requested Beer: '+beer);
});
}这里的getBeerByIdBridge即为我们定义的“桥”,它的作用是将抽象的click事件与具体的getBeerById实现连接起来。同时,它将事件源的ID以及自定义的处理函数(如console.log输出)作为参数传递给getBeerById函数,从而实现了抽象与实现的解耦。
这个例子相对简单,接下来我们来看一个更为复杂的实战案例。
实战:XHR连接队列
实战XHR连接队列
我们需要构建一个请求队列,其中存放多个Ajax请求。使用队列的主要目的是确保先加入的请求优先被处理。此外,该队列还应支持暂停请求、删除请求、重试请求以及对各个请求订阅事件等功能。
基础核心函数 在正式编写队列逻辑之前,我们先定义几个核心的封装函数。首先是异步请求函数的封装:
代码如下:
var asyncRequest = (function () {
function handleReadyState(o, callback) {
var poll = window.setInterval(
function () {
if (o && o.readyState == 4) {
window.clearInterval(poll);
if (callback) {
callback(o);
}
}
},
50
);
}
var getXHR = function () {
var http;
try {
http = new XMLHttpRequest;
getXHR = function () {
return new XMLHttpRequest;
};
}
catch (e) {
var msxml = [
'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'
];
for (var i = 0, len = msxml.length; i < len; ++i) {
try {
http = new ActiveXObject(msxml[i]);
getXHR = function () {
return new ActiveXObject(msxml[i]);
};
break;
}
catch (e) { }
}
}
return http;
};
return function (method, uri, callback, postData) {
var http = getXHR();
http.open(method, uri, true);
handleReadyState(http, callback);
http.send(postData || null);
return http;
};
})();
上述封装的自执行函数是一个通用的Ajax请求函数,熟悉Ajax开发的读者应该能够轻松理解其逻辑。
接下来,我们定义一个通用的添加方法(函数)的工具:
代码如下:
Function.prototype.method = function (name, fn) {
this.prototype[name] = fn;
return this;
};最后,再添加两个关于数组的方法,分别用于遍历和筛选:
代码如下:
if (!Array.prototype.forEach) {
Array.method('forEach', function (fn, thisObj) {
var scope = thisObj || window;
for (var i = 0, len = this.length; i < len; ++i) {
fn.call(scope, this[i], i, this);
}
});
}
if (!Array.prototype.filter) {
Array.method('filter', function (fn, thisObj) {
var scope = thisObj || window;
var a = [];
for (var i = 0, len = this.length; i < len; ++i) {
if (!fn.call(scope, this[i], i, this)) {
continue;
}
a.push(this[i]);
}
return a;
});
}由于部分新型浏览器已经原生支持这两种功能(或者某些类库已提供支持),因此代码中先进行了判断,若已支持则不再重复处理。
观察者系统 观察者在队列的事件处理过程中扮演着关键角色,允许在队列处理的不同阶段(成功、失败、挂起)订阅相应的事件:
代码如下:
window.DED = window.DED || {};
DED.util = DED.util || {};
DED.util.Observer = function () {
this.fns = [];
}
DED.util.Observer.prototype = {
subscribe: function (fn) {
this.fns.push(fn);
},
unsubscribe: function (fn) {
this.fns = this.fns.filter(
function (el) {
if (el !== fn) {
return el;
}
}
);
},
fire: function (o) {
this.fns.forEach(
function (el) {
el(o);
}
);
}
};
队列主要实现代码 首先,订阅队列的主要属性和事件委托机制:
代码如下:
DED.Queue = function () {
// 包含请求的队列.
this.queue = [];
// 使用Observable对象在3个不同的状态上,以便可以随时订阅事件
this.onComplete = new DED.util.Observer;
this.onFailure = new DED.util.Observer;
this.onFlush = new DED.util.Observer;
// 核心属性,可以在外部调用的时候进行设置
this.retryCount = 3;
this.currentRetry = 0;
this.paused = false;
this.timeout = 5000;
this.conn = {};
this.timer = {};
};
随后,通过DED.Queue.method的链式调用,为队列添加了多个可用方法:
代码如下:
DED.Queue.
method('flush', function () {
// flush方法
if (!this.queue.length > 0) {
return;
}
if (this.paused) {
this.paused = false;
return;
}
var that = this;
this.currentRetry++;
var abort = function () {
that.conn.abort();
if (that.currentRetry == that.retryCount) {
that.onFailure.fire();
that.currentRetry = 0;
} else {
that.flush();
}
};
this.timer = window.setTimeout(abort, this.timeout);
var callback = function (o) {
window.clearTimeout(that.timer);
that.currentRetry = 0;
that.queue.shift();
that.onFlush.fire(o.responseText);
if (that.queue.length == 0) {
that.onComplete.fire();
return;
}
// recursive call to flush
that.flush();
};
this.conn = asyncRequest(
this.queue[0]['method'],
this.queue[0]['uri'],
callback,
this.queue[0]['params']
);
}).
method('setRetryCount', function (count) {
this.retryCount = count;
}).
method('setTimeout', function (time) {
this.timeout = time;
}).
method('add', function (o) {
this.queue.push(o);
}).
method('pause', function () {
this.paused = true;
}).
method('dequeue', function () {
this.queue.pop();
}).
method('clear', function () {
this.queue = [];
});虽然代码量较大,但折叠后可以看出,实际上只是在队列上定义了flush、setRetryCount、setTimeout、add、pause、dequeue和clear等方法。
简单调用
代码如下:
var q = new DED.Queue;
// 设置重试次数高一点,以便应付慢的连接
q.setRetryCount(5);
// 设置timeout时间
q.setTimeout(1000);
// 添加2个请求.
q.add({
method: 'GET',
uri: '/path/to/file.php?ajax=true'
});
q.add({
method: 'GET',
uri: '/path/to/file.php?ajax=true&woe=me'
});
// flush队列
q.flush();
// 暂停队列,剩余的保存
q.pause();
// 清空.
q.clear();
// 添加2个请求.
q.add({
method: 'GET',
uri: '/path/to/file.php?ajax=true'
});
q.add({
method: 'GET',
uri: '/path/to/file.php?ajax=true&woe=me'
});
// 从队列里删除最后一个请求.
q.dequeue();
// 再次Flush
q.flush();
完整示例中的桥接体现
桥接呢?
在上述调用代码中并未直接体现桥接模式,但查看下面的完整示例,你会发现桥接无处不在:
代码如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Ajax Connection Queue</title>
<script src="utils.js"></script>
<script src="queue.js"></script>
<script type="text/javascript">
addEvent(window, 'load', function () {
// 实现.
var q = new DED.Queue;
q.setRetryCount(5);
q.setTimeout(3000);
var items = $('items');
var results = $('results');
var queue = $('queue-items');
// 在客户端保存跟踪自己的请求
var requests = [];
// 每个请求flush以后,订阅特殊的处理步骤
q.onFlush.subscribe(function (data) {
results.innerHTML = data;
requests.shift();
queue.innerHTML = requests.toString();
});
// 订阅时间处理步骤
q.onFailure.subscribe(function () {
results.innerHTML += ' <span style="color:red;">Connection Error!</span>';
});
// 订阅全部成功的处理步骤x
q.onComplete.subscribe(function () {
results.innerHTML += ' <span style="color:green;">Completed!</span>';
});
var actionDispatcher = function (element) {
switch (element) {
case 'flush':
q.flush();
break;
case 'dequeue':
q.dequeue();
requests.pop();
queue.innerHTML = requests.toString();
break;
case 'pause':
q.pause();
break;
case 'clear':
q.clear();
requests = [];
queue.innerHTML = '';
break;
}
};
var addRequest = function (request) {
var data = request.split('-')[1];
q.add({
method: 'GET',
uri: 'bridge-connection-queue.php?ajax=true&s=' + data,
params: null
});
requests.push(data);
queue.innerHTML = requests.toString();
};
addEvent(items, 'click', function (e) {
var e = e || window.event;
var src = e.target || e.srcElement;
try {
e.preventDefault();
}
catch (ex) {
e.returnValue = false;
}
actionDispatcher(src.id);
});
var adders = $('adders');
addEvent(adders, 'click', function (e) {
var e = e || window.event;
var src = e.target || e.srcElement;
try {
e.preventDefault();
}
catch (ex) {
e.returnValue = false;
}
addRequest(src.id);
});
});
</script>
<style type="text/css" media="screen">
body
{
font: 100% georgia,times,serif;
}
h1, h2
{
font-weight: normal;
}
#queue-items
{
height: 1.5em;
}
#add-stuff
{
padding: .5em;
background: #ddd;
border: 1px solid #bbb;
}
#results-area
{
padding: .5em;
border: 1px solid #bbb;
}
</style>
</head>
<body id="example">
<div id="doc">
<h1>
异步联接请求</h1>
<div id="queue-items">
</div>
<div id="add-stuff">
<h2>向队列里添加新请求</h2>
<ul id="adders">
<li><a href="#" id="action-01">添加 "01" 到队列</a></li>
<li><a href="#" id="action-02">添加 "02" 到队列</a></li>
<li><a href="#" id="action-03">添加 "03" 到队列</a></li>
</ul>
</div>
<h2>队列控制</h2>
<ul id='items'>
<li><a href="#" id="flush">Flush</a></li>
<li><a href="#" id="dequeue">出列Dequeue</a></li>
<li><a href="#" id="pause">暂停Pause</a></li>
<li><a href="#" id="clear">清空Clear</a></li>
</ul>
<div id="results-area">
<h2>
结果:
</h2>
<div id="results">
</div>
</div>
</div>
</body>
</html>在这个示例中,你可以执行flush队列、暂停队列、删除队列中的请求、清空队列等各种操作,同时相信你也已经体会到了桥接模式带来的便利与威力。
总结
总结
桥接模式的优点十分明显,我们仅列举其中几个主要优势:
1.分离接口和实现部分,一个实现未必不变地绑定在一个接口上,抽象类(函数)的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现,同将抽象和实现也进行了充分的解耦,也有利于分层,从而产生更好的结构化系统。 2.提高可扩充性 3.实现细节对客户透明,可以对客户隐藏实现细节。
同时桥接模式也有自己的缺点:
大量的类将导致开发成本的增加,同时在性能方面可能也会有所减少。