代理模式的定義是:爲一個對象提供代理,來控制對這個對象的訪問。javascript
在某些狀況下,直接訪問對象不方便或者對訪問對象加強一些功能,可使用到代理模式。好比想請一個明星來辦一場商業演出,通常都是聯繫明星的經紀人,那麼經紀人就是明星的代理。前端
在這個故事中,假設妹子是girl對象,小明想要給妹子送花。因爲妹子只有一個,就直接經過一個對象字面量表示。java
class Gift {}
class Person {
constructor(name, job) {
this.name = name;
this.job = job;
}
sendGift(target) {
const gift = new Gift();
target.receiveGift(this, gift);
}
}
const girl = {
receiveGift(sender, gift) {
console.log(`from ${sender.name}`, sender, gift);
}
}
const xiaoming = new Person('小明', '程序員');
xiaoming.sendGift(girl); // from 小明 Person {name: "小明", job: "程序員"} Gift {}
複製代碼
如今妹子收到禮物了,也知道了小明的姓名和工做。但是追求妹子的人不少,妹子一我的收不過來啊,這時候妹子就須要一個代理對象了,稱爲proxyGirl。react
class Gift {}
class Person {
constructor(name, job) {
this.name = name;
this.job = job;
}
sendGift(target) {
const gift = new Gift();
target.receiveGift(this, gift);
}
}
const proxyGirl = {
receiveGift(...args) {
girl.receiveGift(...args);
}
}
const girl = {
receiveGift(sender, gift) {
console.log(`from ${sender.name}`, sender, gift);
}
}
const xiaoming = new Person('小明', '程序員');
xiaoming.sendGift(proxyGirl);
複製代碼
這裏結果和上述同樣,所作的就是增長了一個代理對象。這必然會增長一些代碼,增長程序的複雜度。它的好處在於能夠經過代理對象,去控制對目標對象的直接訪問(見定義)。程序員
好比在proxyGirl中去進行一些過濾。ajax
const proxyGirl = {
receiveGift(...args) {
const sender = args[0];
if(sender.job !== '程序員') {
girl.receiveGift(...args);
} else {
throw sender;
}
}
}
複製代碼
若是給妹子送禮物的是程序員,那麼把他扔出去。緩存
從上述例子中,能夠看到兩種代理方式的影子。代理對象能夠幫目標對象過濾掉一些請求,好比職業是程序員的,或者沒房沒車的。這種代理叫作保護代理。bash
另外,假設禮物價值不菲,在程序中new Gift也是一個代價昂貴的操做。那麼咱們能夠把這個操做交給代理類去執行。代理類首先過濾掉不符合條件的人,而後去new Gift,這是代理類的另外一種形式,叫作虛擬代理,也叫作動態代理。虛擬代理把一些開銷很大的對象,延遲到真正須要它的時候纔去建立(相似於單例模式中的惰性單例)。服務器
const proxyGirl = {
receiveGift(sender) {
const sender = args[0];
if(sender.job !== '程序員') {
const gift = new Gift();
girl.receiveGift(sender, gift); // 不改變目標對象的參數
} else {
throw sender;
}
}
}
複製代碼
前端開發中,直接給img設置目標src不是一個好的作法。當圖片體積比較大的時候,不能第一時間顯示出來,就會形成空白,這很顯然不是一個好的體驗。常見的作法是給圖片預先設置一個loading圖(或分辨率較低的原圖),而後用異步的方式加載圖片,加載好後再替換原圖片的url。這種場景就很適合時候虛擬代理(給目標對象增長loading功能)。網絡
const myImage = {
setSrc: (ele, src) => {
ele.src = src;
}
}
const proxyImage = {
checkEle: ele => {
if(ele.tagName !== 'IMG') {
throw '這個對象只能代理img標籤';
}
},
setSrc: (ele, src) => {
// 初始設置爲loading圖片
this.checkEle();
// 設置loading
ele.src = 'loading.png';
// 圖片下載好了以後替換原圖的url
const img = new Image();
img.src = src;
img.onload = () => {
myImage.setSrc(ele, src);
}
}
}
const img = document.querySelector('.some-img');
proxyImage.setSrc(img, 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1547377781665&di=6f7dd28462a295f04da213e190728681&imgtype=0&src=http%3A%2F%2Fb.zol-img.com.cn%2Fdesk%2Fbizhi%2Fstart%2F2%2F1363857521405.jpg')
複製代碼
咱們經過代理對象proxyImage間接訪問目標對象myImage,並添加過濾標籤功能,增長loading功能。
上面的實現咱們徹底能夠放在myImage對象中。
const myImage = {
setSrc: (ele, src) => {
if(ele.tagName !== 'IMG') {
throw '這個對象只能代理img標籤';
}
// 設置loading
ele.src = 'loading.png';
// 圖片下載好了以後替換原圖的url
const img = new Image();
img.src = src;
img.onload = () => {
ele.src = src;
}
}
}
複製代碼
好像也沒有什麼問題。代碼確實能正常工做,並達到了預期的效果。不過它違反了單一職責原則。職責被定義爲「引發變化的緣由」,就是說有且只有一個緣由引發對象的變化。若是多個緣由都能引發對象變化,那麼說明這個對象承擔了過多的職責,它將變得巨大,而且職責之間相互耦合,那麼必將致使高耦合低內聚的設計。咱們在處理其中一個職責時,有可能由於強耦合性影響到另外一個職責的實現。這對於測試來講也是很是不便的。
另外,在面向對象的設計中,大多數狀況下,若是違反其餘任何原則,同時將違背開放封閉原則。將來,若是網速很是快,再也不須要loading了,那麼咱們要移除loading,就必須修改myImage對象。
實際上,myImage對象中,只須要實現給img標籤添加src的功能。loading功能和過濾功能只是錦上添花。若是能把這些加強功能放在另外一個對象裏面,天然是極好的設計。因而代理的做用在這裏就體現出來了。代理加強過濾標籤和loading功能,操做完成後,把請求從新交給本體myImage。
代理對象和本體對象的接口(參數)應該保持一致。 上述例子中,若是不須要加強功能的時候,咱們徹底可使用myImage對象替換proxyImage對象。在客戶看來,代理對象和本體是一致的,客戶並不須要知道代理和本體的區別,這樣有兩個好處。
第二點讓我想到了里氏替換原則。
里氏代換原則是面向對象設計的基本原則之一。 里氏代換原則中說,任何基類能夠出現的地方,子類必定能夠出現。
代理類能夠看作是繼承了目標類,並對其進行了加強。
此外,上面一直在談論代理對象。注意:函數也是一個對象。
const myImage = (ele, src) => ele.src = src;
const proxyImage = (ele, src) => {
// loading功能,省略
myImage(ele, src);
}
複製代碼
若是頁面上有n多個checkbox,點擊一個checkbox都要發送一個請求,請求攜帶checkbox的uniqueId參數。頻繁的網絡請求會帶給服務器壓力。最初的代碼是這樣的:
const postRequest = id => {
// 發送請求操做,忽略
}
const checkbox = document.querySelectorAll('input[type="checkbox"]');
for(let i = 0; i < checkbox.length; i++) {
checkbox[i].onClick = function() {
postMessage(this.unique_id);
}
}
複製代碼
那麼怎樣經過虛擬代理合並呢。
const postRequest = id => {
// 發送請求操做,忽略
}
const proxyPostRequest = (() => {
const caches = [];
let timer;
return id => {
caches.push(id);
if(timer) {
return;
}
timer = setTimeout(() => {
postRequest(caches.join(','));
caches.length = 0;
timer = null;
}, 2000);
}
})()
const checkbox = document.querySelectorAll('input[type="checkbox"]');
for(let i = 0; i < checkbox.length; i++) {
checkbox[i].onClick = function() {
proxyPostRequest(this.unique_id);
}
}
複製代碼
proxyPostRequest是一個IIFE,返回一個閉包。請求不要同時發出,而是兩秒後合併id,只發送一次。
proxyPostRequest應用了函數柯里化(function currying)的思想。
currying又稱爲部分求值。一個currying的函數首先會接受一些參數,接受了這些參數以後,並不會當即求值,而是繼續返回另一個函數,剛纔傳入的參數在函數造成的閉包中被保存起來。待到函數被真正須要求值的時候,以前傳入的全部參數都會被一次性用於求值。
緩存代理能夠爲一些開銷大的運算結果提供暫時的存儲。在下次計算時,若是傳遞進來的參數跟以前一致,則能夠直接返回以前緩存的結果。這須要不含反作用的函數(若是函數中有Date.now()、Math.random()、外部變量等參與了計算,那麼可能會致使緩存的結果並不正確)。
// 假設這裏的add有巨大的計算量(狗頭)
var add = (...args) => {
return args.reduce((prev, curr) => {
return prev += curr;
}, 0)
}
const proxyAdd = (() => {
const caches = [];
return (...args) => {
let value = caches[args.join(',')];
if(value !== undefined) {
return value;
}
return caches[args.join(',')] = add(...args);
}
})()
proxyAdd(1, 2, 3);
proxyAdd(1, 2, 3);
proxyAdd(1, 2, 3, 4);
複製代碼
實際開發中,如某些展現性的表格,分頁的數據不須要重複拉取。拉取一次後,換緩存下來,下次使用能夠直接訪問了。react開發中能夠避免重複調動action。
// action/xxx.js
const fetchPageData = (id) => (() => {
const caches = [];
return dispatch => {
if(caches[id] !== undefined) {
return;
}
var data = fetchxxx(id);
if(data) {
caches[id] = id;
dispatch(storeData({
type: xxx,
data,
}))
}
return data;
}
})()
複製代碼
顯然這裏可使用緩存代理達到請求。
上述緩存加速結果例子中,只能緩存加法的結果。若是須要緩存乘法的結果,那麼又要建立一個proxyMulti的函數。這會寫重複代碼。可使用工廠模式來建立緩存代理。
return args.reduce((prev, curr) => {
return prev += curr;
}, 0)
}
const multi = (...args) => {
return args.reduce((prev, curr) => {
return prev *= curr;
}, 1)
}
const createProxyFactory = fn => {
const caches = [];
return (...args) => {
let value = caches[args.join(',')];
if(value !== undefined) {
return value;
}
return caches[args.join(',')] = fn.apply(this, args);
}
}
const proxyAdd = createProxyFactory(add);
proxyAdd(1, 2, 3, 4);
const proxyMulti = createProxyFactory(multi);
proxyMulti(1, 2, 3, 4);
複製代碼
代理模式的變種很是多,限於篇幅以及在js的適用性,一下代理簡單介紹一下。
代理模式的定義是:爲一個對象提供代理,來控制對這個對象的訪問。
優勢:
缺點:
和其餘模式的區別
一、和適配器模式的區別:適配器模式主要改變所考慮對象的接口,而代理模式不能改變所代理類的接口。
二、和裝飾器模式的區別:裝飾器模式爲了加強功能,而代理模式是爲了加以控制(文中給圖片鞥家loading的時候,彷佛區分不是那麼明顯)。
代理模式分類龐雜,在JS中最經常使用的是保護代理、虛擬代理和緩存代理(文中都用到了)。雖然代理模式很是有用,但不須要預先猜想是否須要使用代理,當發現不方便直接訪問某個對象的時候,再編寫代理也不遲。