1 代理模式的定義
代理模式:為一個物件提供一個代理用品或者佔位符,以便控制對它的訪問。生活中有很多這樣的例子:
- 請明星代言,實際是聯絡明星的經紀人,來建立商戶 與 明星的關係
- 二手市場買房,實際聯絡的是中介公司來搭建買房與買房之間的橋樑
因此,可以這樣理解:當客戶不方便直接訪問一個物件或者不滿足需要的時候,提供一個替身物件來控制這個物件的訪問,客戶實際訪問的是替身物件。替身物件對請求做出一些處理後,再講請求轉交給本體物件。
#2 代理example 我們這裡使用小明送花給小美來舉例子。
- 小明找到他與小美兩個共同的好朋友小丑,讓她幫忙送花
- 小丑不會直接送給小美,而是會找小美心情好的時候送,增加成功機率
- 小美收到鮮花接收了小明
function Flower(name) {
this.name = name;
}
// 小明使用物件導向的方式建立
var Xiaoming = function() {
}
Xiaoming.prototype.sendFlower = function(xiaochou) {
var flower = new Flower('玫瑰花');
xiaochou.recieveFlower(flower);
}
// 小丑使用javascript的物件即類的概念建立
var xiaochou = {
recieveFlower: function(flower) {
var goodDay = xiaomei.listenerGoodDay();
if (goodDay) {
xiaomei.recieveFlower(flower);
} else {
console.log('小美心情不好,因此沒有送');
}
}
};
var xiaomei = {
// 小美心情隨機
listenerGoodDay: function() {
var random = parseInt(Math.random() * 10);
return random % 2 === 0 ? true : false;
},
recieveFlower: function(flower) {
console.log(`小美看到是${flower.name}, 接收了小明`);
}
};
var xiaoming = new Xiaoming();
xiaoming.sendFlower(xiaochou);
複製程式碼
3 保護代理和虛擬代理
從上面的例子中,我們可以知道xiaochou可以幫助小美過濾掉小美不喜歡的型別:例如沒有寶馬車,長得矮,長得醜的,小丑可以直接拒絕。這種代理叫做保護代理。 假如鮮花價格比較昂貴,new Flower()代價很高,可以把new Flower()操作交給代理xiaochou,她可以根據xiaomei的心情好的時候再去new Flower(),避免鮮花枯萎浪費。這叫做虛擬代理。虛擬代理把一些開銷很大的物件,延遲到真正需要它的時候才建立。
var xiaochou = {
recieveFlower: function() {
var goodDay = xiaomei.listenerGoodDay();
if (goodDay) {
var flower = new Flowe();
xiaomei.recieveFlower(flower);
} else {
console.log('小美心情不好,因此沒有送');
}
}
};
複製程式碼
保護代理用於控制不同許可權的物件對目標物件的訪問,在javascript中並不容易實現保護代理,因為我們無法判斷誰訪問了某個物件。而虛擬代理是常用的一種代理模式。
4 虛擬代理載入圖片
在web開發中,圖片預載入是一種常用的技術。如果給某個img標籤直接新增src屬性,如果某個圖片過大或者網速差,則圖片的位置會有一段時間是空白的。常見的做法是在載入圖片完畢之前使用一個loading圖片。等到圖片載入完畢後替換。下面我們使用虛擬代理來完成。
- 建立一個本體ImageSrc方法
- 建立一個代理ProxyImageSrc方法
// 本體
var ImageSrc = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
};
})();
// 代理
var ProxyImageSrc = (function ProxyImageSrc() {
var img = new Image();
// 頁面載入完畢後,將img上存放的src設定到ImageSrc物件上
img.onload = function() {
ImageSrc.setSrc = this.src;
};
return {
setSrc: function(src) {
// 載入中使用loading圖片
ImageSrc.setSrc('http://img4.imgtn.bdimg.com/it/u=1972873509,2904368741&fm=27&gp=0.jpg');
img.src = src;
}
};
})();
// 呼叫,設定src
ProxyImageSrc.setSrc('http://img05.tooopen.com/images/20150820/tooopen_sy_139205349641.jpg');
複製程式碼
其實實現該功能,即使不引入任何模式也能夠辦到,例如通過下面的方式:
var ImageSrc = (function() {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
var img = new Image();
img.onload = function(src) {
imgNode = this.src;
}
return {
setSrc: function(src) {
imgNode.src = 'loading.png';
img.src = src;
}
}
})();
ImageSrc.setSrc('complete.png');
複製程式碼
該物件承擔了多項職責。負責了圖片的預載入處理,圖片完成後的設定。違背了物件導向設計的原則單一職責原則
。當物件承擔更多職責,意味著物件會變的巨大,引起它變化的情況會變的很多。在設計程式碼的時候應該高內聚低耦合,儘量符合單一職責。
5 代理和本體介面一致
當某一天我們不再需要預載入,那麼就不需要設定代理物件,可以直接請求本體。其中關鍵點是:代理物件與本體都對外提供setSrc。在客戶看來,本體與代理是一致的。代理接收的請求過程對於使用者來說是透明的,使用者不清楚代理與本體的區別,這樣的好處是:
- 使用者可以放心請求代理,他只需要關心是否能夠得到預期的結果
- 在任何使用本體的地方都能夠替換成為使用代理
在java語言中,代理和本體需要顯示實現同一個介面,在保證它們擁有同樣的方法的同時,面向介面程式設計迎合依賴倒置原則,通過介面向上轉型,避開編譯器的型別檢查,使得代理與本體將來能夠被替換使用。 javascript語言是動態型別語言,我們可以通過duck typing
來檢查代理和本體是否擁有setSrc方法,而大多數時候不會檢查。
6 虛擬代理合並http請求
下圖是我們在網頁中上傳檔案的列表,當點選checkbox則上傳檔案。加入手速比較快的人,1秒鐘點選4,5次是不成問題的。那麼相當於1秒以內,就需要與伺服器進行4次請求,頻繁的網路請求會帶來相當大的開銷。因此我們可以等待2秒以後,將2秒以內點選了的檔案一起傳送給伺服器,如果對實時要求不是非常高的系統,2秒的延遲不會出現太大的副作用,卻能大大減輕伺服器的壓力。
// 本體,同步檔案的方法
var synchronousFile = function(ids) {
console.log(`開始同步${ids} 檔案`);
}
// 代理,非同步呼叫方法
var proxySynchronousFile = (function() {
var cacheIds = [];
var timer = null;
return function(id) {
cacheIds.push(id);
if (timer) {
return;
}
timer = setTimeout(function() {
synchronousFile(cacheIds.join(',')); // 2秒後向伺服器傳送請求
clearTimeout(timer);
timer = null;
cache.length = 0; // 清空集合。
},2000);
}
})();
// 註冊上傳檔案點選事件
var checkboxs = document.getElementsByTagName('input');
checkboxs.map(function(checkbox) {
checkbox.onclick = function(event) {
if (this.checked === true) {
proxySynchronousFile(this.id);
}
}
});
複製程式碼
7 虛擬代理實現快取
快取代理可以為一些開銷大的運算結果提供暫時儲存,在下次傳遞進來的引數如果與之前某一次的一致,直接返回儲存的結果。上面儲存一個通過ajax請求獲取的資料。
// 本體
var getAjaxData = function(arg) {
$.ajax('url', function(data) {
return data;
});
}
// 代理
var proxyData = (function() {
var cache = [];
return function(arg) {
if (arg in cache) {
return cache[arg];
} else {
return cache[arg] = getAjaxData(arg);
}
}
})();
// 呼叫,第一次呼叫會將資料存放在cache中,第二次呼叫會直接使用快取中的資料
var returnData = proxyData(1);
var retrunData2 = proxyData(1);
複製程式碼
8 用高階函式建立代理
通過傳入高階函式這種更加靈活的方式,可以為各種計算方法建立快取代理。
// 本體
var mult = function() {
return [].reduce.call(arguments, function(x, y) {
return x * y;
}, 1)
}
// 本體
var plus = function() {
return [].reduce.call(arguments, function(x, y) {
return x + y;
}, 0)
}
// 代理
var calculateProxy = function(fn) {
var cache = [];
return function() {
var args = [].join.call(arguments, ',');
if (args in cache) {
return cache[args];
} else {
return cache[args] = fn.apply(this, arguments);
}
};
};
// 呼叫
var proxyMult = calculateProxy(mult);
var proxyPlus = calculateProxy(plus);
console.log(proxyMult(2, 4, 5));
console.log(proxyPlus(2, 4, 5));
複製程式碼
9 小結
代理模式包括許多小分類,在javascript中常用的是虛擬代理與快取代理。雖然代理模式非常有用,但是我們在編寫業務程式碼的時候,往往不需要去預先猜測是否需要使用代理模式。而是當真正發現不方便直接訪問某個物件的時候,再編寫代理不遲。