定義
??代理模式是為一個(gè)對(duì)象提供一個(gè)占位符,以便控制對(duì)它的訪問。
描述
??代理模式是一種非常有意義的模式队贱,在生活中可以找到很多代理模式的場(chǎng)景欣除。比如住拭,明星都有經(jīng)紀(jì)人作為代理。如果想請(qǐng)明星來辦一場(chǎng)商業(yè)演出历帚,只能聯(lián)系他的經(jīng)紀(jì)人滔岳。經(jīng)紀(jì)人會(huì)把商業(yè)演出的細(xì)節(jié)和報(bào)酬都談好之后,再把合同交給明星簽挽牢。
??代理模式的關(guān)鍵是當(dāng)客戶不方便直接訪問一個(gè)對(duì)象或者不滿足需要的時(shí)候谱煤,提供一個(gè)替身對(duì)象來控制對(duì)這個(gè)對(duì)象的訪問,客戶實(shí)際上訪問的是替身對(duì)象禽拔。替身對(duì)象對(duì)請(qǐng)求做出一些處理之后刘离,再把請(qǐng)求轉(zhuǎn)交給本體對(duì)象。
應(yīng)用
??下面從小明送花追女孩的案例來熟悉代理模式的結(jié)構(gòu)奏赘,先看看不用代理模式的情況:
class Flower{
constructor(name){
this.name = name
}
}
let xiaoming = {
sendFlower: function( target ){
let flower = new Flower('玫瑰花')
target.receiveFlower( flower )
}
}
let girl = {
receiveFlower: function( flower ){
console.log( '收到' + flower.name )
}
};
xiaoming.sendFlower( girl )
引入女孩的閨蜜作為代理者:
class Flower {
constructor(name) {
this.name = name
}
}
let xiaoming = {
sendFlower: function (target) {
let flower = new Flower('玫瑰花')
target.receiveFlower(flower)
}
}
let closeFriend = {
receiveFlower(flower) {
girl.listenGoodMood(() => {
girl.receiveFlower(flower)
})
}
}
let girl = {
receiveFlower: function (flower) {
console.log('收到' + flower.name)
},
listenGoodMood: function (fn) {
setTimeout(() => {fn()}, 10000)
}
}
xiaoming.sendFlower(closeFriend)
??現(xiàn)在改變故事的背景設(shè)定寥闪,假設(shè)當(dāng)girl在心情好的時(shí)候收到花,小明表白成功的幾率有60%磨淌,而當(dāng)girl在心情差的時(shí)候收到花疲憋,小明表白的成功率無限趨近于0。
??通過這個(gè)案例梁只,我們可以看到代理模式的價(jià)值缚柳。小明跟girl剛剛認(rèn)識(shí)兩天,還無法辨別girl什么時(shí)候心情好搪锣。如果不合時(shí)宜地把花送給girl秋忙,花被直接扔掉的可能性很大。但是girl的closeFriend卻很了解girl构舟,所以小明只管把花交給closedFriend灰追,closeFriend會(huì)監(jiān)聽girl的心情變化,然后選擇girl心情好的時(shí)候把花轉(zhuǎn)交給gril狗超。
??雖然這只是個(gè)虛擬的例子弹澎,但可以從中找到兩種代理模式的身影。girl的閨蜜可以幫組girl過濾掉一些請(qǐng)求努咐,比如送花的人年齡太大苦蒿、長(zhǎng)得太丑、太窮等渗稍,這種請(qǐng)求就可以直接在代理處被拒絕掉佩迟,這種代理叫做保護(hù)代理团滥。另外,現(xiàn)實(shí)中的花價(jià)值不菲报强,程序世界里灸姊,new Flower()
也是一個(gè)代價(jià)昂貴的操作。把new Flower()
的操作交給代理closeFriend去執(zhí)行躺涝,代理會(huì)選擇在girl心情好時(shí)再執(zhí)行new Flower()
厨钻,這是代理模式的另一種形式,叫作虛擬代理坚嗜。虛擬代理把一些開銷很大的對(duì)象夯膀,延遲到真正需要它的時(shí)候才去創(chuàng)建。代碼如下:
let closedFriend = {
receiveFlower() {
girl.listenGoodMood(() => {
let flower = new Flower('玫瑰花')
girl.receiveFlower(flower)
})
}
}
圖片預(yù)加載
??在Web開發(fā)中苍蔬,圖片預(yù)加載是一種常用的技術(shù)诱建,如果直接給某個(gè)img標(biāo)簽節(jié)點(diǎn)設(shè)置src屬性,由于圖片過大或者網(wǎng)絡(luò)不佳碟绑,圖片的位置往往有段時(shí)間會(huì)是一片空白俺猿。常見的做法是先用一張loading圖片占位,然后用異步的方式加載圖片格仲,等圖片加載好了再把它填充到img節(jié)點(diǎn)里押袍,這種場(chǎng)景就很適合使用虛擬代理。
下面來實(shí)現(xiàn)這個(gè)虛擬代理凯肋,首先創(chuàng)建一個(gè)普通的本體對(duì)象谊惭,這個(gè)對(duì)象負(fù)責(zé)往頁面中創(chuàng)建一個(gè)img標(biāo)簽,并且提供一個(gè)對(duì)外的setSrc接口侮东,外界調(diào)用這個(gè)接口圈盔,便可以給該img標(biāo)簽設(shè)置。
let myImage = (function(){
let imgNode = document.createElement( 'img' )
document.body.appendChild( imgNode )
return {
setSrc: function( src ){
imgNode.src = src
}
}
})()
myImage.setSrc( 'coming.png' )
??設(shè)置網(wǎng)速很差的情況下悄雅,通過myImage驱敲,setSrc給該img元素設(shè)置src,可以看出宽闲,在圖片被加載好之前众眨,頁面有很長(zhǎng)的空白時(shí)間,用戶體驗(yàn)很不好容诬。
??現(xiàn)在引入代理對(duì)象proxyImage围辙,通過這個(gè)代理對(duì)象,在圖片被真正加載好之前放案,頁面中將出現(xiàn)一張占位的loading.gif,來提示用戶圖片正在加載矫俺。
let myImage = (function(){
let imgNode = document.createElement( 'img' )
document.body.appendChild( imgNode )
return {
setSrc: function( src ){
imgNode.src = src
}
}
})()
let proxyImage = (function(){
let img = new Image
img.onload = function(){
myImage.setSrc( this.src )
}
return {
setSrc: function( src ){
myImage.setSrc( 'loading.gif' )
img.src = src
}
}
})()
proxyImage.setSrc( 'coming.png' )
代理模式的意義
??也許讀者有疑問吱殉,不過是實(shí)現(xiàn)一個(gè)小小的圖片預(yù)加載功能掸冤,及時(shí)不引入任何模式也能辦到,那么引入代理模式的好處究竟在哪里友雳?不使用代理稿湿,則圖片預(yù)加載的函數(shù)實(shí)現(xiàn)代碼如下:
let MyImage = (function(){
let imgNode = document.createElement( 'img' )
document.body.appendChild( imgNode )
let img = new Image
img.onload = function(){
imgNode.src = img.src
}
return {
setSrc: function( src ){
imgNode.src = 'loading.gif'
img.src = src
}
}
})()
MyImage.setSrc( 'coming.png' )
??這就要講到面向?qū)ο笤O(shè)計(jì)的原則——單一職責(zé)原則。
單一職責(zé)原則指的是押赊,就一個(gè)類(通常也包括對(duì)象和函數(shù)等)而言饺藤,應(yīng)該僅有一個(gè)引起它變化的原因。如果一個(gè)對(duì)象承擔(dān)了多項(xiàng)職責(zé)流礁,就意味著這個(gè)對(duì)象將變得巨大涕俗,引起它變化的原因可能會(huì)有多個(gè)。面向?qū)ο笤O(shè)計(jì)鼓勵(lì)將行為分布到細(xì)粒度的對(duì)象之中神帅,如果一個(gè)對(duì)象承擔(dān)的職責(zé)過多再姑,等于把這些職責(zé)耦合到了一起,這種耦合會(huì)導(dǎo)致脆弱和低內(nèi)聚的設(shè)計(jì)找御。當(dāng)變化發(fā)生時(shí)元镀,設(shè)計(jì)可能會(huì)遭到意外的破壞。
職責(zé)被定義為“引起變化的原因”霎桅。上面代碼中的MyImage對(duì)象除了負(fù)責(zé)給img節(jié)點(diǎn)設(shè)置src外栖疑,還要負(fù)責(zé)預(yù)加載圖片。在處理其中一個(gè)職責(zé)時(shí)滔驶,有可能因?yàn)槠鋸?qiáng)耦合性影響另外一個(gè)職責(zé)的實(shí)現(xiàn)遇革。
另外,在面向?qū)ο蟮某绦蛟O(shè)計(jì)中瓜浸,大多數(shù)情況下澳淑,若違反其他任何原則,同時(shí)將違反開放——封閉原則插佛。如果只是從網(wǎng)絡(luò)上獲取一些體積很小的圖片杠巡,或者5年后的網(wǎng)速快到根本不再需要預(yù)加載,可能希望把預(yù)加載圖片的這段代碼從MyImage對(duì)象里刪掉雇寇。這時(shí)候就不得不改動(dòng) MyImage 對(duì)象了氢拥。實(shí)際上,需要的只是給img節(jié)點(diǎn)設(shè)置src锨侯,預(yù)加載圖片只是一個(gè)錦上添花的功能嫩海。如果能把這個(gè)操作放在另一個(gè)對(duì)象里面,自然是一個(gè)非常好的方法囚痴。于是代理的作用在這里就體現(xiàn)出來了叁怪,代理負(fù)責(zé)預(yù)加載圖片,預(yù)加載的操作完成之后深滚,把請(qǐng)求重新交給本體MyImage奕谭。
縱觀整個(gè)程序涣觉,并沒有改變或者增加MyImage的接口,但是通過代理對(duì)象血柳,實(shí)際上給系統(tǒng)添加了新的行為官册。這是符合開放——封閉原則的。給img節(jié)點(diǎn)設(shè)置 src 和圖片預(yù)加載這兩個(gè)功能难捌, 被隔離在兩個(gè)對(duì)象里膝宁,它們可以各自變化而不影響對(duì)方。何況就算有一天不再需要預(yù)加載根吁, 那么只需要改成請(qǐng)求本體而不是請(qǐng)求代理對(duì)象即可员淫。
代理和本體接口的一致性
??代理對(duì)象和本體都對(duì)外提供了setSrc方法,在客戶看來婴栽,代理對(duì)象和本體是一致的满粗, 代理接手請(qǐng)求的過程對(duì)于用戶來說是透明的,用戶并不清楚代理和本體的區(qū)別愚争,這樣做有兩個(gè)好處:1映皆、用戶可以放心地請(qǐng)求代理,只關(guān)心是否能得到想要的結(jié)果轰枝;2捅彻、在任何使用本體的地方都可以替換成使用代理。
虛擬代理合并HTTP請(qǐng)求
??在Web開發(fā)中鞍陨,也許最大的開銷就是網(wǎng)絡(luò)請(qǐng)求步淹。假設(shè)在做一個(gè)文件同步的功能,選中一個(gè)checkbox時(shí)诚撵,它對(duì)應(yīng)的文件就會(huì)被同步到另外一臺(tái)備用服務(wù)器上面缭裆。
??首先,在頁面中放置好這些checkbox節(jié)點(diǎn):
<body>
<input type="checkbox" id="1"></input>1
<input type="checkbox" id="2"></input>2
<input type="checkbox" id="3"></input>3
<input type="checkbox" id="4"></input>4
<input type="checkbox" id="5"></input>5
<input type="checkbox" id="6"></input>6
<input type="checkbox" id="7"></input>7
<input type="checkbox" id="8"></input>8
<input type="checkbox" id="9"></input>9
</body>
??接下來寿烟,給這些checkbox綁定點(diǎn)擊事件澈驼,并且在點(diǎn)擊的同時(shí)往另一臺(tái)服務(wù)器同步文件。
let synchronousFile = function( id ){
console.log( '開始同步文件筛武,id 為: ' + id )
}
let checkboxs = document.getElementsByTagName( 'input' );
for ( let i = 0, c; c = checkboxs[ i++ ]; ){
c.onclick = function(){
if ( this.checked === true ){
synchronousFile( this.id )
}
}
}
??當(dāng)選中3個(gè)checkbox的時(shí)候缝其,依次往服務(wù)器發(fā)送了3次同步文件的請(qǐng)求∨橇可以預(yù)見内边,如此頻繁的網(wǎng)絡(luò)請(qǐng)求將會(huì)帶來相當(dāng)大的開銷。
??解決方案是可以通過一個(gè)代理函數(shù)proxySynchronousFile來收集一段時(shí)間之內(nèi)的請(qǐng)求待锈,最后一次性發(fā)送給服務(wù)器漠其。比如等待2秒之后才把這2秒之內(nèi)需要同步的文件ID打包發(fā)給服務(wù)器,如果不是對(duì)實(shí)時(shí)性要求非常高的系統(tǒng),2秒的延遲不會(huì)帶來太大副作用辉懒,卻能大大減輕服務(wù)器的壓力阳惹。
let synchronousFile = function( id ){
console.log( '開始同步文件,id 為: ' + id )
}
let proxySynchronousFile = (function(){
let cache = [], // 保存一段時(shí)間內(nèi)需要同步的ID
timer // 定時(shí)器
return function( id ){
cache.push( id )
if ( timer ){ // 保證不會(huì)覆蓋已經(jīng)啟動(dòng)的定時(shí)器
return
}
timer = setTimeout(function(){
synchronousFile( cache.join( ',' ) ) // 2 秒后向本體發(fā)送需要同步的ID 集合
clearTimeout( timer ) // 清空定時(shí)器
timer = null
cache.length = 0 // 清空ID 集合
}, 2000 )
}
})()
let checkboxs = document.getElementsByTagName( 'input' )
for ( let i = 0, c; c = checkboxs[ i++ ] ){
c.onclick = function(){
if ( this.checked === true ){
proxySynchronousFile( this.id )
}
}
}
緩存代理
??緩存代理可以為一些開銷大的運(yùn)算結(jié)果提供暫時(shí)的存儲(chǔ)眶俩,在下次運(yùn)算時(shí),如果傳遞進(jìn)來的參數(shù)跟之前一致快鱼,則可以直接返回前面存儲(chǔ)的運(yùn)算結(jié)果颠印。
??下面是一個(gè)計(jì)算乘積的例子,先創(chuàng)建一個(gè)用于求乘積的函數(shù):
var mult = function(){
console.log( '開始計(jì)算乘積' );
var a = 1;
for ( var i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
mult( 2, 3 ); // 輸出:6
mult( 2, 3, 4 ); // 輸出:24
??然后加入緩存代理函數(shù):
let proxyMult = (function(){
let cache = {}
return function(){
let args = Array.prototype.join.call( arguments, ',' )
if ( args in cache ){
return cache[ args ]
}
return cache[ args ] = mult.apply( this, arguments )
}
})()
proxyMult( 1, 2, 3, 4 ) // 輸出:24
proxyMult( 1, 2, 3, 4 ) // 輸出:24
??當(dāng)?shù)诙握{(diào)用proxyMult(1,2,3,4)的時(shí)候抹竹,本體mult函數(shù)并沒有被計(jì)算线罕,proxyMult直接返回了之前緩存好的計(jì)算結(jié)果。通過增加緩存代理的方式窃判,mult函數(shù)可以繼續(xù)專注于自身的職責(zé)——計(jì)算乘積钞楼,緩存的功能是由代理對(duì)象實(shí)現(xiàn)的。
在項(xiàng)目中常常遇到分頁的需求袄琳,同一頁的數(shù)據(jù)理論上只需要去后臺(tái)拉取一次询件,這些已經(jīng)拉取到的數(shù)據(jù)在某個(gè)地方被緩存之后,下次再請(qǐng)求同一頁的時(shí)候唆樊,便可以直接使用之前的數(shù)據(jù)宛琅。顯然這里也可以引入緩存代理,實(shí)現(xiàn)方式跟計(jì)算乘積的例子差不多逗旁,唯一不同的是嘿辟,請(qǐng)求數(shù)據(jù)是個(gè)異步的操作,無法直接把計(jì)算結(jié)果放到代理對(duì)象的緩存中片效,而是要通過回調(diào)的方式红伦。
其他代理模式
??代理模式的變體種類非常多,還包括以下幾種:
1. 防火墻代理:控制網(wǎng)絡(luò)資源的訪問淀衣,保護(hù)主題不讓“壞人”接近昙读。
??2.遠(yuǎn)程代理:為一個(gè)對(duì)象在不同的地址空間提供局部代表。
3. 保護(hù)代理:用于對(duì)象應(yīng)該有不同訪問權(quán)限的情況舌缤。
4. 智能引用代理:取代了簡(jiǎn)單的指針箕戳,它在訪問對(duì)象時(shí)執(zhí)行一些附加操作,比如計(jì)算一個(gè)對(duì)象被引用的次數(shù)国撵。
5陵吸、寫時(shí)復(fù)制代理:通常用于復(fù)制一個(gè)龐大對(duì)象的情況。寫時(shí)復(fù)制代理延遲了復(fù)制的過程介牙,當(dāng)對(duì)象被真正修改時(shí)壮虫,才對(duì)它進(jìn)行復(fù)制操作。寫時(shí)復(fù)制代理是虛擬代理的一種變體,DLL(操作系統(tǒng)中的動(dòng)態(tài)鏈接庫)是其典型運(yùn)用場(chǎng)景囚似。
小結(jié)
??代理模式包括許多小分類剩拢,在javascript開發(fā)中最常用的是虛擬代理和緩存代理。雖然代理模式非常有用饶唤,但在編寫業(yè)務(wù)代碼時(shí)徐伐,往往不需要去預(yù)先猜測(cè)是否需要使用代理模式。當(dāng)真正發(fā)現(xiàn)不方便直接訪問某個(gè)對(duì)象的時(shí)候募狂,再編寫代理也不遲办素。
參考文獻(xiàn)
《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》