ES6+ Proxy
1. 前言
本節我們將學習 ES6 的新增知識點 ——Proxy,Proxy 是代理的意思。Proxy 是一個對象,用于定義基本操作的自定義行為(如屬性查找、賦值、枚舉、函數調用等)。這是 MDN 上的定義,但是不容易理解,想要理解 Proxy 我們首先需要知道什么是代理?
在日常開發中比較常見的代理常見有,使用 Charles 代理抓包、nginx 服務器的反向代理,以及 VPN 等,都用到了代理。什么是代理呢?我們先看一張圖:
上圖是客戶端訪問網絡的示意圖,客戶端不能直接訪問網絡,它只能先訪問代理服務器,只有代理服務器才能有權限訪問,然后代理服務器把客戶端請求的信息轉發給目標服務器,最后代理服務器在接收到目標服務器返回的結果再轉發給客戶端,這樣就完成了整個請求的響應過程。這是現在大多數服務器的架構,我們可以把上圖的 Proxy Server 理解為 Nginx。代理有正向代理和反向代理,有興趣的小伙伴可以去深入了解一下。
本節說的 Proxy 就是作用在 JavaScript 中的一種代理服務,代理的過程其實就是一種對數據的劫持過程,Proxy 可以對我們定義的對象的屬性進行劫持,當我們訪問或設置屬性時,會去調用對應的鉤子執行。在 ES5 中我們曾學習過 Object.defineProperty()
它的作用和 Proxy 是相同的,但是 Object.defineProperty()
存在一些性能問題,Proxy 對其進行了升級和擴展更加方便和易用。本節我們將學習 Proxy 的使用。
2. Object.defineProperty()
在學習 Proxy 之前,我們先來回歸一下 ES5 中的 Object.defineProperty()
,接觸過前端框架的同學應該都知道 Vue 和 React,其中 Vue 中的響應式數據底層就是使用 Object.defineProperty()
這個 API 來實現的。下面是 Object.defineProperty()
的語法。
Object.defineProperty(obj, prop, descriptor)
Object.defineProperty()
會接收三個參數:
- obj 需要觀察的對象;
- prop 是 obj 上的屬性名;
- descriptor 對 prop 屬性的描述。
當我們去觀察一個對象時需要在 descriptor 中去定義屬性的描述參數。在 descriptor 對象中提供了 get 和 set 方法,當我們訪問或設置屬性值時會觸發對應的函數。
var obj = {};
var value = undefined;
Object.defineProperty(obj, "a", {
get: function() {
console.log('value:', value)
return value;
},
set: function(newValue) {
console.log('newValue:', newValue)
value = newValue;
},
enumerable: true,
configurable: true
});
obj.a; // value: undefined
obj.a = 20; // newValue: 20
上面的代碼中,我們使用一個變量 value 來保存值,這里需要注意的是,不能直接使用 obj 上的值,否則就會出現死循環。
Object.defineProperty()
是 Vue2 的核心, Vue2 在初始化時會對數據進行劫持,如果劫持的屬性還是對象的話需要遞歸劫持。下面我們把 Vue2 中數據劫持的核心代碼寫出來。
var data = {
name: 'imooc',
lession: 'ES6 Wiki',
obj: {
a: 1
}
}
observer(data);
function observer(data) {
if (typeof data !== 'object' || data == null) {
return;
}
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = obj[key];
defineReactive(obj, key, value);
}
}
function defineReactive(obj, key, value) {
observer(value);
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
if (newValue === value) return;
observer(newValue);
value = newValue;
}
})
}
上面代碼的核心是 defineReactive 方法,它是遞歸的核心函數,用于重新定義對象的讀寫。從上面的代碼中我們發現 Object.defineProperty()
是有缺陷的,當觀察的數據嵌套非常深時,這樣是非常耗費性能的,這也是為什么現在 Vue 的作者極力推廣 Vue3 的原因之一,Vue3 的底層使用了 Proxy 來代替 Object.defineProperty()
那 Proxy 具體有什么好處呢?
3. Proxy
首先我們來看下 Proxy 是如何使用的,語法:
const p = new Proxy(target, handler)
Proxy 對象是一個類,需要通過 new 去實例化一個 Proxy 對象,它接收的參數比較簡單,只有兩個:
- target:需要使用 Proxy 進行觀察的目標對象;
- handler:對目標對象屬性進行處理的對象,包含了處理屬性的回調函數等。
const handler = {
get: function(obj, prop) {
return obj[prop];
},
set: function(obj, prop, value) {
return obj[prop] = value;
}
};
const p = new Proxy({}, handler);
p.a = 1;
console.log(p.a, p.b); // 1, undefined
對比上面的 Object.defineProperty()
API 直觀的看 Proxy 做了一些精簡,把對象、屬性和值作為 get 和 set 的參數傳入進去,不必考慮死循環的問題了。這是直觀的感受。
上面我們使用了 Object.defineProperty()
API 簡單地實現了 Vue2 的響應式原理,那么 Vue 使用 Proxy 是怎么實現的呢?它帶來了哪些好處呢?下面我們看實現源碼:
var target = {
name: 'imooc',
lession: 'ES6 Wiki',
obj: {
a: 1
}
}
var p = reactive(target);
console.log(p.name); // 獲取值: imooc
p.obj.a = 10; // 獲取值: {a : 1}
console.log(p.obj.a); // 獲取值: {a : 10}
function reactive(target) {
return createReactiveObject(target)
}
function createReactiveObject(target) {
// 判斷如果不是一個對象的話返回
if (!isObject(target)) return target
// target觀察前的原對象; proxy觀察后的對象:observed
observed = new Proxy(target, {
get(target, key, receiver) {
const res = target[key];
console.log('獲取值:', res)
// todo: 收集依賴...
return isObject(res) ? reactive(res) : res
},
set(target, key, value, receiver) {
target[key] = value;
}
})
return observed
}
上面的代碼是從 Vue3 中摘出來的 reactive 函數的實現,我們可以直觀地看到沒有對 target 進行遞歸循環去創建觀察對象。而且,當我們對 obj 下的 a 屬性設置值時,執行 get 函數,這是為什么呢?這就是 Proxy 的優點,在對 obj 下屬性設置值時,首先需要調用 set 方法獲取 target 下 obj 的值,然后判斷 obj 又是一個對象再去調用 reactive 函數進行觀察。這樣就不需要遞歸地去對嵌套數據進行觀察了,而是在獲取值的時候,判斷獲取的值是不是一個對象,這樣極大地節約了資源。
4. 小結
本節主要通過代理和 Object.defineProperty()
API 的學習來理解 ES6 的新增知識點 ——Proxy,并且通過 Vue2 和 Vue3 實現響應式原理來對比 Object.defineProperty()
和 Proxy 的優缺點,從而更深入地理解 Proxy。