JS帶你深入領略Proxy的世界
1. Proxy 的基本結構
Proxy 的基本使用方式:
/** * target: 表示要代理的目標,可以是object,array,function型別 * handler: 是一個物件,可以編寫各種代理的方法 */ const proxy = new Proxy(target,handler);
例如我們想要代理一個物件,可以通過設定 get 和 set 方法來代理獲取和設定資料的操作:
const person = { name: 'wenzi',age: 20,}; const personProxy = new Proxy(person,{ get(target,key,receiver) { console.log(`get value by ${key}`); return target[key]; },set(target,value) { console.log(`set ${key},old value ${target[key]} to ${value}`); target[key] = value; },});
Proxy 僅僅是一個代理,personProxy 上有 person 所有的屬性和方法。我們通過personProxy獲取和設定 name 時,就會有相應的 log 輸出:
personProxy.name; // "wenzi" // log: get value by name personProxy.name = 'hello'; // log: set name,old value wenzi to hello
並且通過 personProxy 設定資料時,代理的原結構裡的資料也會發生變化。我們列印下 person,可以發現欄位 name 的值 也變成了hello:
console.log(person); // {name: "hello",age: 20}
Proxy 的第 2 個引數 handler 除了可以設定 get 和 set 方法外,還有更多豐富的方法:
1.get(target,propKey,receiver):攔截物件屬性的讀取,比如 proxy.foo 和 proxy['foo']。
2.set(target,value,receiver):攔截物件屬性的設定,比如 proxy.foo = v 或 proxy['foo'] = v,返回一個布林值。
3.has(target,propKey):攔截 propKey in proxy 的操作,返回一個布林值。
4.deleteProperty(target,propKey):攔截 delete proxy[propKey]的操作,返回一個布林值。
5.ownKeys(target):攔截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 迴圈,返回一個數組。該方法返回目標物件所有自身的屬性的屬性名,而 Object.keys()的返回結果僅包括目標物件自身的可遍歷屬性。
6.getOwnPropertyDescriptor(target,propKey):攔截 Object.getOwnPropertyDescriptor(proxy,propKey),返回屬性的描述物件。
7.defineProperty(target,propDesc):攔截 Object.defineProperty(proxy,propDesc)、Object.defineProperties(proxy,propDescs),返回一個布林值。
8.preventExtensions(target):攔截 Object.preventExtensions(proxy),返回一個布林值。
9.getPrototypeOf(target):攔截 Object.getPrototypeOf(proxy),返回一個物件。
10.isExtensible(target):攔截 Object.isExtensible(proxy),返回一個布林值。
11.setPrototypeOf(target,proto):攔截 Object.setPrototypeOf(proxy,proto),返回一個布林值。如果目標物件是函式,那麼還有兩種額外操作可以攔截。
12.apply(target,object,args):攔截 Proxy 例項作為函式呼叫的操作,比如 proxy(...args)、proxy.call(object,...args)、proxy.apply(...)。
13.construct(target,args):攔截 Proxy 例項作為建構函式呼叫的操作,比如 new proxy(...args)。
如我們通過 delete 刪除其中一個元素時,可以通過deleteProperty()方法來攔截這個操作。還是上面代理 person 的程式碼,我們新增一個 deleteProperty:
const person = { name: 'wenzi',{ // 忽略get和set方法,與上面一樣 // ... deleteProperty(target,receiver) { console.log(`delete key ${key}`); delete target[key]; },});
當執行 delete 操作時:
delete personProxy['age']; // log: delete key age
2. Proxy 與 Reflect
Proxy 與 Reflect 可以說形影不離了,Reflect 裡所有的方法和使用方式與 Proxy 完全一樣。
例如上面 Proxy 裡的 get(),set()和 deleteProperty()方法我們都是直接操作原代理物件的,這裡我們改成使用Reflect
來操作:
const personProxy = new Proxy(person,receiver) { console.log(`get value by ${key}`); return Reflect.get(target,receiver); },receiver) { console.log(`set ${key},old value ${target[key]} to ${value}`); return Reflect.set(target,deleteProperty(target,receiver) { console.log(`delete key ${key}`); return Reflect.deleteProperty(target,});
可以發現完美地實現這些功能。
3. 代理陣列
我們在之前的文章 vue 中對陣列特殊的操作 中,討論過 Vue 為什麼沒有使用Object.defineProperty來劫持資料,而是重寫了 Array 原型鏈上的幾個方法,通過這幾個方法來實現 Vue 模板中資料的更新。
但若 Proxy 的話,就可以直接代理陣列:
const arr = [1,2,3,4]; const arrProxy = new Proxy(arr,receiver) { console.log('arrProxy.get',target,key); return Reflect.get(target,receiver) { console.log('arrProxy.set',value); return Reflect.set(target,key) { console.log('arrProxy.deleteProperty',key); return Reflect.deleteProperty(target,key); },});
現在我們再來操作一下代理後的陣列 arrProxy 看下:
arrProxy[2] = 22; // arrProxy.set (4) [1,4] 2 22 arrProxy[3]; // arrProxy.get (4) [1,22,4] 3 delete arrProxy[2]; // arrProxy.deleteProperty (4) [1,4] 2 arrProxy.push(5); // push操作比較複雜,這裡進行了多個get()和set()操作 arrProxy.length; // arrProxy.get (5) [1,empty,4,5] length
可以看到無論獲取、刪除還是修改資料,都可以感知到。還有陣列原型鏈上的一些方法,如:
1.push()
2.pop()
3.shift()
4.unshift()
5.splice()
6.sort()
7.reverse()
也都能通過 Proxy 中的代理方法劫持到。
concat()方法比較特殊的是,他是一個賦值操作,並不改變原陣列,因此在呼叫 concat()方法運算元組時,如果沒有賦值操作,那麼這裡只有 get()攔截到。
4. 代理函式
Proxy 中還有一個apply()方法,是表示自己作為函式呼叫時,被攔截的操作。
const getSum = (...args) => { if (!args.every((item) => typeof item === 'number')) { throw new TypeError('引數應當均為number型別'); } return args.reduce((sum,item) => sum + item,0); }; const fnProxy = new Proxy(getSum,{ /** * @params {Fuction} target 代理的物件 * @params {any} ctx 執行的上下文 * @params {any} args 引數 */ apply(target,ctx,args) { console.log('ctx',ctx); console.log(`execute fn ${getSum.name},args: ${args}`); return Reflect.apply(target,args); },});
執行 fnProxy:
// 10,ctx為undefined,log: execute fn getSum,args: 1,4 fnProxy(1,4); // ctx為undefined,Uncaught TypeError: 引數應當均為number型別 fnProxy(1,'4'); // 10,ctx為window,4 fnProxy.apply(window,[1,4]); // 6,3 fnProxy.call(window,1,3); // 6,ctx為person,3 fnProxy.apply(person,3]);
5. 一些簡單的應用場景
我們知道 Vue3 裡已經用 Proxy 重寫了響應式系統,mobx 也已經用了 Proxy 模式。在可見的未來,會有更多的 Proxy 的應用場景,我們這裡也稍微講解幾個。
5.1 統計函式被呼叫的上下文和次數
這裡我們用 Proxy 來代理函式,然後函式被呼叫的上下文和次數。
const countExecute = (fn) => { let count = 0; return new Proxy(fn,{ apply(target,args) { ++count; console.log('ctx上下文:',ctx); console.log(`${fn.name} 已被呼叫 ${count} 次`); return Reflect.apply(target,args); },}); };
現在我們來代理下剛才的getSum()方法:
const getSum = (...args) => { if (!args.every((item) => typeof item === 'number')) { throw new TypeError('引數應當均為numbe程式設計客棧r型別'); } return args.reduce((sum,0); }; www.cppcns.com const useSum = countExecute(getSum); useSum(1,3); // getSum 已被呼叫 1 次 useSum.apply(window,[2,4]); // getSum 已被呼叫 2 次 useSum.call(person,5); // getSum 已被呼叫 3 次
5.2 實現一個防抖功能
基於上面統計函式呼叫次數的功能,也給我們實現一個函式的防抖功能添加了靈感。
const throttleByProxy = (fn,rate) => { let lastTime = 0; return new Proxy(fn,args) { const now = Date.now(); if (now - lastTime > rate) { lastTime = now; return Reflect.apply(target,args); } },}); }; const logTimeStamp = () => cxBjeoonsole.log(Date.now()); window.addEventListener('scroll',throttleByProxy(lohttp://www.cppcns.comgTimeStamp,300));
logTimeStamp()至少需要 300ms 才能執行一次。
5.3 實現觀察者模式
我們在這裡實現一個最簡單類 mobx 觀察者模式。
const list = new Set();
const observe = (fn) => list.add(fn);
const observable = (obj) => {
return new Proxy(obj,{
set(target,receiver) {
const result = Reflect.set(target,receiver);
list.forEach((observer) => observer());
return result;www.cppcns.com
},});
};
const person = observable({ name: 'wenzi',age: 20 });
const App = () => {
console.log(`App -> name: ${person.name},age: ${person.age}`);
};
observe(App);
person就是使用 Proxy 創建出來的代理物件,每當 person 中的屬性發生變化時,就會執行 App()函式。這樣就實現了一個簡單的響應式狀態管理。
6. Proxy 與 Object.defineProperty 的對比
上面很多例子用Object.defineProperty也都是可以實現的。那麼這兩者都各有什麼優缺點呢?
6.1 Object.defineProperty 的優劣
Object.defineProperty的相容性可以說比 Proxy 要好很多,出特別低的 IE6,IE7 瀏覽器外,其他瀏覽器都有支援。
但 Object.defineProperty 支援的方法很多,並且主要是基於屬性進行攔截的。因此在 Vue2 中只能重寫 Array 原型鏈上的方法,來運算元組。
6.2 Proxy 的優劣
Proxy與上面的正好相反,Proxy 是基於物件來進行代理的,因此可代理更多的型別,例如 Object,Array,Function 等;而且代理的方法也多了很多。
劣勢就是相容性不太好,即使用 polyfill,也無法完美的實現。
7. 總結
Proxy 能實現的功能還有很多,後面我們也會繼續進行探索,並且儘可能去了解下基於 Proxy 實現的類庫,例如 mobx5 的原始碼和實現原理等。
以上就是js帶你深入領略Proxy的世界的詳細內容,更多關於JS中的代理Proxy的資料請關注我們其它相關文章!