看到 Proxy就應該想到代理模式(Proxy Pattern),Proxy
是 Javascript ES2015 標準的一部分,我們應該學會使用它,代理模式是一種設計模式,使用 Proxy
物件可以輕而易舉的在 Javascript 中建立代理模式。然而,使用設計模式並不是目的,目的在於解決實際問題。本文首先會簡單介紹 Proxy
的基本用法,接著將會敘述如何使用 Proxy
建立代理模式並且對我們的應用進行優化。
Proxy 的基本使用
開始學習 Proxy
的使用之前,建議首先對 Reflect 有一定的瞭解,如果很陌生的話,建議先花 1 分鐘瀏覽相關知識。
好了,現在假設已經具備了一定的 Reflect
知識,就開始掌握 Proxy
吧。
基本語法
和 Proxy
相關的方法一共就兩個:
- 構造方法 本文著重討論
Proxy.revocable()
建立一個可撤銷的Proxy
物件,其餘與建構函式類似,理解了Proxy
的構造方法後,該方法與構造方法使用非常類似,本文不再涉及
接下來本文將圍繞 Proxy
的構造方法進行講解。
let p = new Proxy(target, handler);
複製程式碼
引數
-
target 任何型別的物件,包括原生陣列,函式,甚至另一個
Proxy
物件 -
handler 一個物件,其屬性是當執行一個操作時定義代理的行為的函式, 允許的屬性一共 13 種,與
Reflect
的方法名一致
返回
- p
Proxy
物件
注意:
new Proxy
是穩定操作,不會對target
有任何影響。
下面來看幾個代表性的例子,便於加深理解。
代理一個物件字面量:
const target = {};
const handler = {
set: (obj, prop, value) => {
obj[prop] = 2 * value;
},
get: (obj, prop) => {
return obj[prop] * 2;
}
};
const p = new Proxy(target, handler);
p.x = 1; // 使用了 set 方法
console.log(p.x); // 4, 使用了 get 方法
複製程式碼
代理一個陣列:
const p = new Proxy(
['Adela', 'Melyna', 'Lesley'],
{
get: (obj, prop) => {
if (prop === 'length') return `Length is ${obj[prop]}.`;
return `Hello, ${obj[prop]}!`;
}
}
);
console.log(p.length) // Length is 3.
console.log(p[0]); // Hello, Adela
console.log(p[1]); // Hello, Melyna
console.log(p[2]); // Hello, Lesley
複製程式碼
代理一個普通函式:
const foo = (a, b, c) => {
return a + b + c;
}
const pFoo = new Proxy(foo, {
apply: (target, that, args) => {
const grow = args.map(x => x * 2);
const inter = Reflect.apply(target, that, grow);
return inter * 3;
}
});
pFoo(1, 2, 3); // 36, (1 * 2 + 2 * 2 + 3 * 2) * 3
複製程式碼
代理建構函式
class Bar {
constructor(x) {
this.x = x;
}
say() {
console.log(`Hello, x = ${this.x}`);
}
}
const PBar = new Proxy(Bar, {
construct: (target, args) => {
const obj = new Bar(args[0] * 2);
return obj;
}
});
const p = new PBar(1);
p.say(); // Hello, x = 2
複製程式碼
Proxy
的基本用法無出其上,可 Proxy
的真正用途還沒有顯現出來,接下來結合設計模式中的一種模式 —— 代理模式 —— 進一步討論。
使用 Proxy 建立代理模式
從上面的例子並不能看出 Proxy
給我們帶來了什麼便利,需要實現的功能完全可以在原函式內部進行實現。既然如此,使用代理模式的意義是什麼呢?
- 遵循“單一職責原則”,物件導向設計中鼓勵將不同的職責分佈到細粒度的物件中,
Proxy
在原物件的基礎上進行了功能的衍生而又不影響原物件,符合鬆耦合高內聚的設計理念 - 遵循“開放-封閉原則”,代理可以隨時從程式中去掉,而不用對其他部分的程式碼進行修改,在實際場景中,隨著版本的迭代可能會有多種原因不再需要代理,那麼就可以容易的將代理物件換成原物件的呼叫
達到上述兩個原則有一個前提就是代理必須符合“代理和本體介面一致性”原則:代理和原物件的輸入和輸出必須是一致的。這樣對於使用者來說,代理就是透明的,代理和原物件在不改動其他程式碼的條件下是可以被相互替換的。
代理模式的用途很廣泛,這裡我們看一個快取代理的例子。
首先建立一個 Proxy
的包裝函式,該函式接受需要建立代理的目標函式為第一個引數,以快取的初值為第二個引數:
const createCacheProxy = (fn, cache = new Map()) => {
return new Proxy(fn, {
apply(target, context, args) {
const argsProp = args.join(' ');
if (cache.has(argsProp)) {
console.log('Using old data...');
return cache.get(argsProp);
}
const result = fn(...args);
cache.set(argsProp, result);
return result;
}
});
};
複製程式碼
然後我們使用乘法函式 mult
去建立代理並呼叫:
const mult = (...args) => args.reduce((a, b) => a * b);
const multProxy = createCacheProxy(mult);
multProxy(2, 3, 4); // 24
multProxy(2, 3, 4); // 24, 輸出 Using old data
複製程式碼
也可以使用其他的函式:
const squareAddtion = (...args) => args.reduce((a, b) => a + b ** 2, 0);
const squareAddtionProxy = createCacheProxy(squareAddtion);
squareAddtionProxy(2, 3, 4); // 29
squareAddtionProxy(2, 3, 4); // 29, 輸出 Using old data
複製程式碼
對於上面這個例子,有三點需要注意:
- 對於檢測是否存在舊值的過程較為粗暴,實際應用中應考慮是否應該使用更為複雜精確的判斷方法,需要結合實際進行權衡;
createCacheProxy
中的console.log
違背了前文所說的“代理和本體介面一致性”原則,只是為了開發環境更加方便性的除錯,生產環境中必須去掉;multProxy
與squareAdditionProxy
是為了演示使用方法而在這裡使用了相對簡單的演算法和小資料量,但在實際應用中資料量越大、fn
的計算過程越複雜,優化效果越好,否則,優化效果不僅有可能不明顯反而會造成效能下降
代理模式的實際應用
這一節結合幾個具體的例子來加深對代理模式的理解。
函式節流
如果想要控制函式呼叫的頻率,可以使用代理進行控制:
需要實現的基本功能:
const handler = () => console.log('Do something...');
document.addEventListener('click', handler);
複製程式碼
接下來使用 Proxy
進行節流。
首先使用構造建立代理函式:
const createThrottleProxy = (fn, rate) => {
let lastClick = Date.now() - rate;
return new Proxy(fn, {
apply(target, context, args) {
if (Date.now() - lastClick >= rate) {
fn(args);
lastClick = Date.now();
}
}
});
};
複製程式碼
然後只需要將原有的事件處理函式進行一曾包裝即可:
const handler = () => console.log('Do something...');
const handlerProxy = createThrottleProxy(handler, 1000);
document.addEventListener('click', handlerProxy);
複製程式碼
在生產環境中已有多種工具庫實現該功能,不需要我們自己編寫
圖片懶載入
某些時候需要延遲載入圖片,尤其要考慮網路環境惡劣以及比較重視流量的情況。這個時候可以使用一個虛擬代理進行延遲載入。
首先是我們最原始的程式碼:
const img = new Image();
img.src = '/some/big/size/image.jpg';
document.body.appendChild(img);
複製程式碼
為了實現懶載入,建立虛擬圖片節點 virtualImg
並構造建立代理函式:
const createImgProxy = (img, loadingImg, realImg) => {
let hasLoaded = false;
const virtualImg = new Image();
virtualImg.src = realImg;
virtualImg.onload = () => {
Reflect.set(img, 'src', realImg);
hasLoaded = true;
}
return new Proxy(img, {
get(obj, prop) {
if (prop === 'src' && !hasLoaded) {
return loadingImg;
}
return obj[prop];
}
});
};
複製程式碼
最後是將原始的圖片節點替換為代理圖片進行呼叫:
const img = new Image();
const imgProxy = createImgProxy(img, '/loading.gif', '/some/big/size/img.jpg');
document.body.appendChild(imgProxy);
複製程式碼
非同步佇列
這個需求是很常見的:前一個非同步操作結束後再進行下一個非同步操作。這部分我使用 Promise
進行實現。
首先構造一個最為簡單的非同步操作 asyncFunc
:
const callback = () => console.log('Do something...');
const asyncFunc = (cb) => {
setTimeout(cb, 1000);
}
asyncFunc(callback);
asyncFunc(callback);
asyncFunc(callback);
複製程式碼
可以看到控制檯的輸出是 1s 之後,幾乎是同時輸出三個結果:
// .. 1s later ..
Do something...
Do something...
Do something...
複製程式碼
接下來我們使用 Promise
實現非同步佇列:
const createAsyncQueueProxy = (asyncFunc) => {
let promise = null;
return new Proxy(asyncFunc, {
apply(target, context, [cb, ...args]) {
promise = Promise
.resolve(promise)
.then(() => new Promise(resolve => {
Reflect.apply(asyncFunc, this, [() => {
cb();
resolve();
}, ...args]);
}));
}
});
};
複製程式碼
上面這段程式碼通過 Promise
實現了非同步函式佇列,建議在理解了 Promise
之後再理解閱讀上面這段程式碼。
上面這段程式碼測試通過,有兩點需要注意:
promise
的值並不能確定是否為Promise
,需要使用Promise.resolve
方法之後才能使用then
方法Reflect.apply
方法中的第三個引數是陣列,形同與Function.prototype.apply
的第二個引數
然後使用代理進行替換並呼叫:
const timeoutProxy = createAsyncQueueProxy(asynFunc);
timeoutProxy(callback);
timeoutProxy(callback);
timeoutProxy(callback);
複製程式碼
可以看到控制檯的輸出已經像我們期望的那樣: 前一個非同步操作執行完畢之後才會進行下一個非同步操作。
// .. 1s later ..
Do something...
// .. 1s later ..
Do something...
// .. 1s later ..
Do something...
複製程式碼
除了上面這種使用代理的方式實現非同步佇列外,在我的另一篇部落格進階 Javascript 生成器中,還使用了另外一種方式。
結語
本文首先介紹了 ES2015 中關於 Proxy
的基本用法,接著討論了代理模式的使用特點,然後結合實際列舉了幾種常見的使用場景。最後列舉一些比較有價值的參考資料供感興趣的開發者繼續閱讀。