javascript設計模式——代理模式
前面的話
代理模式是為一個對象提供一個占位符,以便控制對它的訪問。 代理模式是一種非常有意義的模式,在生活中可以找到很多代理模式的場景。比如,明星都有經紀人作為代理。如果想請明星來辦一場商業演出,只能聯系他的經紀人。經紀人會把商業演出的細節和報酬都談好之後,再把合同交給明星簽。 代理模式的關鍵是當客戶不方便直接訪問一個對象或者不滿足需要的時候,提供一個替身對象來控制對這個對象的訪問,客戶實際上訪問的是替身對象。替身對象對請求做出一些處理之後,再把請求轉交給本體對象。本文將詳細介紹代理模式
代理模式結構
比如,實現一個小明讓B代替自己向A送花的功能。首先,不引入代理,小明直接送花給A,代碼如下
var Flower = function(){}; var xiaoming = { sendFlower: function( target ){ var flower = new Flower(); target.receiveFlower( flower ); } }; var A = { receiveFlower: function( flower ){ console.log( ‘收到花 ‘ + flower ); } }; xiaoming.sendFlower( A );
接下來,引入代理 B,即小明通過 B 來給 A 送花
var Flower = function(){}; var xiaoming = { sendFlower: function( target){ var flower = new Flower(); target.receiveFlower( flower ); } }; var B = { receiveFlower: function( flower ){ A.receiveFlower( flower ); } };var A = { receiveFlower: function( flower ){ console.log( ‘收到花 ‘ + flower ); } }; xiaoming.sendFlower( B );
現在改變故事的背景設定,假設當A在心情好的時候收到花,小明表白成功的幾率有60%,而當A在心情差的時候收到花,小明表白的成功率無限趨近於0。小明跟A剛剛認識兩天,還無法辨別A什麽時候心情好。如果不合時宜地把花送給A,花被直接扔掉的可能性很大。但是A的朋友B卻很了解A,所以小明只管把花交給B,B會監聽A的心情變化,然後選擇A心情好的時候把花轉交給A,代碼如下:
var Flower = function(){}; var xiaoming = { sendFlower: function( target){ var flower = new Flower(); target.receiveFlower( flower ); } }; var B = { receiveFlower: function( flower ){ A.listenGoodMood(function(){ // 監聽A的好心情 A.receiveFlower( flower ); }); } }; var A = { receiveFlower: function( flower ){ console.log( ‘收到花 ‘ + flower ); }, listenGoodMood: function( fn ){ setTimeout(function(){ // 假設10秒之後A的心情變好 fn(); }, 10000 ); } }; xiaoming.sendFlower( B );
雖然這只是個虛擬的例子,但可以從中找到兩種代理模式的身影
【保護代理】
代理B可以幫助A過濾掉一些請求,比如送花的人中年齡太大的或者沒有寶馬的,這種請求就可以直接在代理B處被拒絕掉。這種代理叫作保護代理
【虛擬代理】
把newFlower的操作交給代理B去執行,代理B會選擇在A心情好時再執行newFlower,這是代理模式的另一種形式,叫作虛擬代理。虛擬代理把一些開銷很大的對象,延遲到真正需要它的時候才去創建。代碼如下:
var B = { receiveFlower: function( flower ){ A.listenGoodMood(function(){ // 監聽A 的好心情 A.receiveFlower( flower ); //延遲創建flower對象 }); } };
圖片預加載
在Web開發中,圖片預加載是一種常用的技術,如果直接給某個img標簽節點設置src屬性,由於圖片過大或者網絡不佳,圖片的位置往往有段時間會是一片空白。常見的做法是先用一張loading圖片占位,然後用異步的方式加載圖片,等圖片加載好了再把它填充到img節點裏,這種場景就很適合使用虛擬代理
下面來實現這個虛擬代理,首先創建一個普通的本體對象,這個對象負責往頁面中創建一個img標簽,並且提供一個對外的setSrc接口,外界調用這個接口,便可以給該img標簽設置
var myImage = (function(){ var imgNode = document.createElement( ‘img‘ ); document.body.appendChild( imgNode ); return { setSrc: function( src ){ imgNode.src = src; } } })(); myImage.setSrc( ‘https://static.xiaohuochai.site/icon/icon_200.png‘ );
現在開始引入代理對象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( ‘https://static.xiaohuochai.site/icon/icon_200.png‘ );
現在通過proxyImage間接地訪問myImage。proxyImage控制了客戶對myImage的訪問,並且在此過程中加入一些額外的操作,比如在真正的圖片加載好之前,先把img節點的src設置為一張本地的loading圖片
單一職責原則
如果不使用代理,則圖片預加載的函數實現代碼如下
var MyImage = (function(){ var imgNode = document.createElement( ‘img‘ ); document.body.appendChild( imgNode ); var img = new Image; img.onload = function(){ imgNode.src = img.src; }; return { setSrc: function( src ){ imgNode.src = ‘loading.gif‘; img.src = src; } } })(); MyImage.setSrc( ‘https://static.xiaohuochai.site/icon/icon_200.png‘ );
下面引入一個面向對象設計的原則——單一職責原則
單一職責原則指的是,就一個類(通常也包括對象和函數等)而言,應該僅有一個引起它變化的原因。如果一個對象承擔了多項職責,就意味著這個對象將變得巨大,引起它變化的原因可能會有多個。面向對象設計鼓勵將行為分布到細粒度的對象之中,如果一個對象承擔的職責過多,等於把這些職責耦合到了一起,這種耦合會導致脆弱和低內聚的設計。當變化發生時,設計可能會遭到意外的破壞
職責被定義為“引起變化的原因”。上面代碼中的MyImage對象除了負責給img節點設置src外,還要負責預加載圖片。在處理其中一個職責時,有可能因為其強耦合性影響另外一個職責的實現
另外,在面向對象的程序設計中,大多數情況下,若違反其他任何原則,同時將違反開放——封閉原則。如果只是從網絡上獲取一些體積很小的圖片,或者5年後的網速快到根本不再需要預加載,可能希望把預加載圖片的這段代碼從MyImage對象裏刪掉。這時候就不得不改動 MyImage 對象了
實際上,需要的只是給img節點設置src,預加載圖片只是一個錦上添花的功能。如果能把這個操作放在另一個對象裏面,自然是一個非常好的方法。於是代理的作用在這裏就體現出來了,代理負責預加載圖片,預加載的操作完成之後,把請求重新交給本體MyImage
縱觀整個程序,並沒有改變或者增加MyImage的接口,但是通過代理對象,實際上給系統添加了新的行為。這是符合開放——封閉原則的。給img節點設置 src 和圖片預加載這兩個功能, 被隔離在兩個對象裏,它們可以各自變化而不影響對方。何況就算有一天不再需要預加載, 那麽只需要改成請求本體而不是請求代理對象即可
【代理和本體接口的一致性】
代理對象和本體都對外提供了setSrc方法,在客戶看來,代理對象和本體是一致的, 代理接手請求的過程對於用戶來說是透明的,用戶並不清楚代理和本體的區別,這樣做有兩個好處:1、用戶可以放心地請求代理,只關心是否能得到想要的結果;2、在任何使用本體的地方都可以替換成使用代理
如果代理對象和本體對象都為一個函數(函數也是對象),函數必然都 能被執行,則可以認為它們也具有一致的“接口”
var myImage = (function(){ var imgNode = document.createElement( ‘img‘ ); document.body.appendChild( imgNode ); return function( src ){ imgNode.src = src; } })(); var proxyImage = (function(){ var img = new Image; img.onload = function(){ myImage( this.src ); } return function( src ){ myImage( ‘file:// /C:/Users/svenzeng/Desktop/loading.gif‘ ); img.src = src; } })(); proxyImage( ‘https://static.xiaohuochai.site/icon/icon_200.png‘ );
合並HTTP請求
在Web開發中,也許最大的開銷就是網絡請求。假設在做一個文件同步的功能,選中一個checkbox時,它對應的文件就會被同步到另外一臺備用服務器上面
首先,在頁面中放置好這些checkbox節點
<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綁定點擊事件,並且在點擊的同時往另一臺服務器同步文件
var synchronousFile = function( id ){ console.log( ‘開始同步文件,id 為: ‘ + id ); }; var checkbox = document.getElementsByTagName( ‘input‘ ); for ( var i = 0, c; c = checkbox[ i++ ]; ){ c.onclick = function(){ if ( this.checked === true ){ synchronousFile( this.id ); } } };
當選中3個checkbox的時候,依次往服務器發送了3次同步文件的請求。可以預見,如此頻繁的網絡請求將會帶來相當大的開銷。解決方案是可以通過一個代理函數proxySynchronousFile來收集一段時間之內的請求,最後一次性發送給服務器。比如等待2秒之後才把這2秒之內需要同步的文件ID打包發給服務器,如果不是對實時性要求非常高的系統,2秒的延遲不會帶來太大副作用,卻能大大減輕服務器的壓力
var synchronousFile = function( id ){ console.log( ‘開始同步文件,id 為: ‘ + id ); }; var proxySynchronousFile = (function(){ var cache = [], // 保存一段時間內需要同步的ID timer; // 定時器 return function( id ){ cache.push( id ); if ( timer ){ // 保證不會覆蓋已經啟動的定時器 return; } timer = setTimeout(function(){ synchronousFile( cache.join( ‘,‘ ) ); // 2 秒後向本體發送需要同步的ID 集合 clearTimeout( timer ); // 清空定時器 timer = null; cache.length = 0; // 清空ID 集合 }, 2000 ); } })(); var checkbox = document.getElementsByTagName( ‘input‘ ); for ( var i = 0, c; c = checkbox[ i++ ]; ){ c.onclick = function(){ if ( this.checked === true ){ proxySynchronousFile( this.id ); } } };
虛擬代理在惰性加載中的應用
假設要加載miniConsole.js這個文件,該文件用於打印log,但該文件很大。所以,通常解決方案是用一個占位的miniConsole代理對象來給用戶提前使用,然後將打印log的請求都包裹在一個函數裏,隨後這些函數將全部放到緩存隊列中,這些邏輯都是在miniConsole代理對象中完成實現的。等用戶按下F2喚出控制臺的時候,才開始加載真正的miniConsole.js的代碼,加載完成之後將遍歷miniConsole代理對象中的緩存函數隊列,同時依次執行它們
未加載真正的miniConsole.js之前的代碼如下:
var cache = []; var miniConsole = { log: function(){ var args = arguments; cache.push( function(){ return miniConsole.log.apply( miniConsole, args ); }); } }; miniConsole.log(1);
當用戶按下F2時,開始加載真正的miniConsole.js,代碼如下:
var handler = function( ev ){ if ( ev.keyCode === 113 ){ var script = document.createElement( ‘script‘ ); script.onload = function(){ for ( var i = 0, fn; fn = cache[ i++ ]; ){ fn(); } }; script.src = ‘miniConsole.js‘; document.getElementsByTagName( ‘head‘ )[0].appendChild( script ); } }; document.body.addEventListener( ‘keydown‘, handler, false ); // miniConsole.js 代碼: miniConsole = { log: function(){ // 真正代碼略 console.log( Array.prototype.join.call( arguments ) ); } };
要註意的是,要保證在F2被重復按下時,miniConsole.js只被加載一次
var miniConsole = (function(){ var cache = []; var handler = function( ev ){ if ( ev.keyCode === 113 ){ var script = document.createElement( ‘script‘ ); script.onload = function(){ for ( var i = 0, fn; fn = cache[ i++ ]; ){ fn(); } }; script.src = ‘miniConsole.js‘; document.getElementsByTagName( ‘head‘ )[0].appendChild( script ); document.body.removeEventListener( ‘keydown‘, handler );// 只加載一次miniConsole.js } }; document.body.addEventListener( ‘keydown‘, handler, false ); return { log: function(){ var args = arguments; cache.push( function(){ return miniConsole.log.apply( miniConsole, args ); }); } } })(); miniConsole.log( 11 ); // 開始打印log // miniConsole.js 代碼 miniConsole = { log: function(){ // 真正代碼略 console.log( Array.prototype.join.call( arguments ) ); } }
緩存代理
緩存代理可以為一些開銷大的運算結果提供暫時的存儲,在下次運算時,如果傳遞進來的參數跟之前一致,則可以直接返回前面存儲的運算結果
下面是一個計算乘積的例子
先創建一個用於求乘積的函數:
var mult = function(){ console.log( ‘開始計算乘積‘ ); 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
然後加入緩存代理函數
var proxyMult = (function(){ var cache = {}; return function(){ var 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
當第二次調用proxyMult(1,2,3,4)的時候,本體mult函數並沒有被計算,proxyMult直接返回了之前緩存好的計算結果。通過增加緩存代理的方式,mult函數可以繼續專註於自身的職責——計算乘積,緩存的功能是由代理對象實現的
在項目中常常遇到分頁的需求,同一頁的數據理論上只需要去後臺拉取一次,這些已經拉取到的數據在某個地方被緩存之後,下次再請求同一頁的時候,便可以直接使用之前的數據。顯然這裏也可以引入緩存代理,實現方式跟計算乘積的例子差不多,唯一不同的是,請求數據是個異步的操作,無法直接把計算結果放到代理對象的緩存中,而是要通過回調的方式
動態創建代理
通過傳入高階函數的方式,可以為各種計算方法創建緩存代理。現在這些計算方法被當作參數傳入一個專門用於創建緩存代理的工廠中,這樣一來,就可以為乘法、加法、減法等創建緩存代理,代碼如下:
/**************** 計算乘積 *****************/ var mult = function(){ var a = 1; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a * arguments[i]; } return a; }; /**************** 計算加和 *****************/ var plus = function(){ var a = 0; for ( var i = 0, l = arguments.length; i < l; i++ ){ a = a + arguments[i]; } return a; }; /**************** 創建緩存代理的工廠 *****************/ 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 proxyMult = createProxyFactory( mult ), proxyPlus = createProxyFactory( plus ); alert ( proxyMult( 1, 2, 3, 4 ) ); // 輸出:24 alert ( proxyMult( 1, 2, 3, 4 ) ); // 輸出:24 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 輸出:10 alert ( proxyPlus( 1, 2, 3, 4 ) ); // 輸出:10
其他代理模式
代理模式的變體種類非常多,還包括以下幾種
1、防火墻代理:控制網絡資源的訪問,保護主題不讓“壞人”接近
2、遠程代理:為一個對象在不同的地址空間提供局部代表
3、保護代理:用於對象應該有不同訪問權限的情況
4、智能引用代理:取代了簡單的指針,它在訪問對象時執行一些附加操作,比如計算一個對象被引用的次數
5、寫時復制代理:通常用於復制一個龐大對象的情況。寫時復制代理延遲了復制的過程,當對象被真正修改時,才對它進行復制操作。寫時復制代理是虛擬代理的一種變體,DLL(操作系統中的動態鏈接庫)是其典型運用場景
代理模式包括許多小分類,在javascript開發中最常用的是虛擬代理和緩存代理。雖然代理模式非常有用,但在編寫業務代碼時,往往不需要去預先猜測是否需要使用代理模式。當真正發現不方便直接訪問某個對象的時候,再編寫代理也不遲
javascript設計模式——代理模式