從“圖片預(yù)加載”認(rèn)識代理設(shè)計模式
本文轉(zhuǎn)載自微信公眾號「DYBOY」,作者DYBOY。轉(zhuǎn)載本文請聯(lián)系DYBOY公眾號。
在現(xiàn)代前端優(yōu)化中,圖片預(yù)加載是一種常見的優(yōu)化方法,預(yù)加載的背后是設(shè)計模式中代理模式的應(yīng)用。
代理模式是為一個對象提供一個代用品或占位符,以便控制對該對象的訪問。
當(dāng)我們需要獲取某個對象的方法或?qū)傩缘臅r候,由于權(quán)限等限制無法獲取,然后通過一個有權(quán)限的代理對象轉(zhuǎn)發(fā)我們的獲取請求,代理對象可對請求預(yù)處理,同時在返回結(jié)果的時候也可以做處理。
生活中的例子:代購、網(wǎng)絡(luò)代理、老板秘書等
一、代理模式分類
代理模式細(xì)分有:
- 虛擬代理:將開銷大的對象延遲到需要時才創(chuàng)建
- 緩存代理:為開銷大的運算結(jié)果提供緩存
- 保護(hù)代理:用于對象應(yīng)該有不同訪問權(quán)限的情況
- 防火墻代理:控制網(wǎng)絡(luò)資源的訪問,保護(hù)主機(jī)不被入侵
- 遠(yuǎn)程代理:為一個對象在不同的地址控件提供局部代表
- 智能引用代理:取代了簡單的指針,在訪問對象執(zhí)行一些附加操作,比如計算一個對象的引用次數(shù)
- 寫時復(fù)制代理:通常用于復(fù)制一個龐大對象的情況,延遲對象復(fù)制過程,當(dāng)對象需要真正修改時才進(jìn)行復(fù)制操作
保護(hù)代理的主要用途就是權(quán)限控制了,但是在 JavaScript 中并不容易實現(xiàn)保護(hù)代理,因為無法判斷訪問對象的來源。
而在 JavaScript 中常用有虛擬代理和緩存代理。
二、虛擬代理實現(xiàn)圖片預(yù)加載
在例如一些多圖的購物網(wǎng)站(淘寶、京東等),都使用了圖片預(yù)加載的技術(shù)。如果直接給 img 標(biāo)簽設(shè)置 src 的值,由于圖片資源過大或者用戶網(wǎng)絡(luò)環(huán)境不佳,就會出現(xiàn)一個長時間白屏,以及圖片至上而下的分段加載的情況,用戶體驗不好,因此,常見的做法是用一張 Loading 小圖占位,等待實際需要加載的圖片加載完成后,再將 Loading 小圖替換成實際的圖片。
比如一個下載圖片,并將 img 標(biāo)簽 append 到 HTML 中,同時提供 setSrc(src) 方法用于設(shè)置圖片資源鏈接方法的“本體對象”實現(xiàn)如下:
- var MyImage = (function() {
- var imgNode = document.createElement('img');
- document.body.appendChild(imgNode);
- return {
- setSrc: function(src) {
- imgNode.src = src;
- }
- }
- })();
使用:
- MyImage.setSrc('http://p1.qhimg.com/bdr/__85/t0156672fda5bb4b5d3.jpg')
在弱網(wǎng)環(huán)境下的效果:
弱網(wǎng)圖片下載體驗效果
有個長時間的白屏,用戶體驗不太友好,為此引入一個 ProxyImage 代理對象,通過該代理對象,在圖片真正被加載完成之前,頁面中顯示一個 loading 動圖來提示用戶圖片正在加載中
- 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('http://p1.qhimg.com/bdr/__85/t0156672fda5bb4b5d3.jpg');
效果:
預(yù)加載下的體驗效果
可以看到我們在沒有修改原有的 MyImage 對象,通過代理對象 ProxyImage 給其增加了預(yù)加載的能力。
實際上,我們可以不用代理就能實現(xiàn)預(yù)加載圖片,為什么還要這么做吶?在對象設(shè)計的原則中有個“單一職責(zé)原則”。如果一個對象承擔(dān)了多項職責(zé),那么對象就會變得較大,引起它變化的原因就可能有多個,那么職責(zé)的耦合度就比較高,會導(dǎo)致脆弱和低內(nèi)聚的設(shè)計,當(dāng)變化發(fā)生時,設(shè)計可能會遭到意外的破壞。
單一職責(zé)原則值得是,一個類應(yīng)該僅有一個引起它變化的原因。職責(zé)被定義為“引起變化的原因”。
上述的做法,MyImage 僅用于添加設(shè)置 img 節(jié)點,ProxyImage 賦予了 MyImage 預(yù)加載的能力,并且對于 MyImage 對象不會有任何侵入性的修改,符合開放封閉原則。如果有一天網(wǎng)絡(luò)足夠快了,我們只需要改為請求本體而不是代理對象即可。不過要實現(xiàn)這種低成本改造,還得保證“代理和本體接口的一致性”。
保證代理和本地的一致性有如下好處:
- 用戶可以放心地請求代理,他只關(guān)心是否能夠獲取想要的結(jié)果;
- 在任何使用本體的地方都可以替換成代理。
三、緩存代理節(jié)省計算開銷
緩存代理為一些開銷大的運算結(jié)果提供臨時存儲,類比于“查表法”。在之前講《從閉包和高階函數(shù)初探JS設(shè)計模式》中有講到“緩存計算”概念,主要是借助“閉包”來實現(xiàn)臨時存儲。
首先是一個基本的乘積函數(shù):
- var mult = function () {
- var a = 1;
- for(var i =0; i< arguments.length; i++) {
- a = a * arguments[i];
- }
- return a;
- }
- mult(1,2,3) // 6
我們都知道乘法的計算開銷比較大,如果涉及到頻繁密集型的計算,那么對于計算機(jī)的計算性能要求就比較高,那么思考是否可以用存儲換時間?因此想到了將計算過的算式緩存,下次再遇到直接獲取值的方式,同樣考慮到職責(zé)單一設(shè)計原則,所以我們可以創(chuàng)建一個緩存代理函數(shù):
- const ProxyMult = (function () {
- const cache = {};
- return function () {
- const args = [].join.call(arguments, ',');
- if (args in cache) {
- return cache[args];
- }
- let sum = 1;
- for (let i = 0; i < arguments.length; i++) {
- sum = sum * arguments[i];
- }
- return cache[args] = sum;
- }
- })();
- ProxyMult(1, 2, 3, 4);
這樣我們就為乘積函數(shù)提供了緩存能力,下次遇到相同算式,就可以不通過計算,類似查表方式拿到計算結(jié)果,再一定程度上提升了計算性能。
同理,Ajax 請求是一個耗時的 IO 操作,比如我們獲取用戶 uid=1 的信息需要 Ajax 請求一次,在一定時間內(nèi),當(dāng)下一次再獲取 uid=1 用戶的信息的時候,我們沒必要再去請求一次,因為用戶的基礎(chǔ)信息并不是頻繁更新的,此時我們可以通過緩存方式拿到上一次獲取到的用戶信息,節(jié)省網(wǎng)絡(luò)帶寬資源,降低網(wǎng)絡(luò)異常帶來出錯的可能性。
在一些場景下我們可以嘗試自行封裝 Ajax 請求,可自行維護(hù)緩存策略。
四、總結(jié)
代理模式的應(yīng)用場景像是一種賦能,保證代理和本體接口一致性的情況下,比如給圖片加載增加“預(yù)加載”能力,給乘法計算增加“緩存”能力。
“青出于藍(lán)而勝于藍(lán)”似乎是一個不錯的解釋。