ES6+ Proxy
1. 前言
本節(jié)我們將學(xué)習(xí) ES6 的新增知識(shí)點(diǎn) ——Proxy,Proxy 是代理的意思。Proxy 是一個(gè)對(duì)象,用于定義基本操作的自定義行為(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。這是 MDN 上的定義,但是不容易理解,想要理解 Proxy 我們首先需要知道什么是代理?
在日常開發(fā)中比較常見的代理常見有,使用 Charles 代理抓包、nginx 服務(wù)器的反向代理,以及 VPN 等,都用到了代理。什么是代理呢?我們先看一張圖:
上圖是客戶端訪問網(wǎng)絡(luò)的示意圖,客戶端不能直接訪問網(wǎng)絡(luò),它只能先訪問代理服務(wù)器,只有代理服務(wù)器才能有權(quán)限訪問,然后代理服務(wù)器把客戶端請(qǐng)求的信息轉(zhuǎn)發(fā)給目標(biāo)服務(wù)器,最后代理服務(wù)器在接收到目標(biāo)服務(wù)器返回的結(jié)果再轉(zhuǎn)發(fā)給客戶端,這樣就完成了整個(gè)請(qǐng)求的響應(yīng)過程。這是現(xiàn)在大多數(shù)服務(wù)器的架構(gòu),我們可以把上圖的 Proxy Server 理解為 Nginx。代理有正向代理和反向代理,有興趣的小伙伴可以去深入了解一下。
本節(jié)說的 Proxy 就是作用在 JavaScript 中的一種代理服務(wù),代理的過程其實(shí)就是一種對(duì)數(shù)據(jù)的劫持過程,Proxy 可以對(duì)我們定義的對(duì)象的屬性進(jìn)行劫持,當(dāng)我們?cè)L問或設(shè)置屬性時(shí),會(huì)去調(diào)用對(duì)應(yīng)的鉤子執(zhí)行。在 ES5 中我們?cè)鴮W(xué)習(xí)過 Object.defineProperty()
它的作用和 Proxy 是相同的,但是 Object.defineProperty()
存在一些性能問題,Proxy 對(duì)其進(jìn)行了升級(jí)和擴(kuò)展更加方便和易用。本節(jié)我們將學(xué)習(xí) Proxy 的使用。
2. Object.defineProperty()
在學(xué)習(xí) Proxy 之前,我們先來回歸一下 ES5 中的 Object.defineProperty()
,接觸過前端框架的同學(xué)應(yīng)該都知道 Vue 和 React,其中 Vue 中的響應(yīng)式數(shù)據(jù)底層就是使用 Object.defineProperty()
這個(gè) API 來實(shí)現(xiàn)的。下面是 Object.defineProperty()
的語法。
Object.defineProperty(obj, prop, descriptor)
Object.defineProperty()
會(huì)接收三個(gè)參數(shù):
- obj 需要觀察的對(duì)象;
- prop 是 obj 上的屬性名;
- descriptor 對(duì) prop 屬性的描述。
當(dāng)我們?nèi)ビ^察一個(gè)對(duì)象時(shí)需要在 descriptor 中去定義屬性的描述參數(shù)。在 descriptor 對(duì)象中提供了 get 和 set 方法,當(dāng)我們?cè)L問或設(shè)置屬性值時(shí)會(huì)觸發(fā)對(duì)應(yīng)的函數(shù)。
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
上面的代碼中,我們使用一個(gè)變量 value 來保存值,這里需要注意的是,不能直接使用 obj 上的值,否則就會(huì)出現(xiàn)死循環(huán)。
Object.defineProperty()
是 Vue2 的核心, Vue2 在初始化時(shí)會(huì)對(duì)數(shù)據(jù)進(jìn)行劫持,如果劫持的屬性還是對(duì)象的話需要遞歸劫持。下面我們把 Vue2 中數(shù)據(jù)劫持的核心代碼寫出來。
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 方法,它是遞歸的核心函數(shù),用于重新定義對(duì)象的讀寫。從上面的代碼中我們發(fā)現(xiàn) Object.defineProperty()
是有缺陷的,當(dāng)觀察的數(shù)據(jù)嵌套非常深時(shí),這樣是非常耗費(fèi)性能的,這也是為什么現(xiàn)在 Vue 的作者極力推廣 Vue3 的原因之一,Vue3 的底層使用了 Proxy 來代替 Object.defineProperty()
那 Proxy 具體有什么好處呢?
3. Proxy
首先我們來看下 Proxy 是如何使用的,語法:
const p = new Proxy(target, handler)
Proxy 對(duì)象是一個(gè)類,需要通過 new 去實(shí)例化一個(gè) Proxy 對(duì)象,它接收的參數(shù)比較簡(jiǎn)單,只有兩個(gè):
- target:需要使用 Proxy 進(jìn)行觀察的目標(biāo)對(duì)象;
- handler:對(duì)目標(biāo)對(duì)象屬性進(jìn)行處理的對(duì)象,包含了處理屬性的回調(diào)函數(shù)等。
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
對(duì)比上面的 Object.defineProperty()
API 直觀的看 Proxy 做了一些精簡(jiǎn),把對(duì)象、屬性和值作為 get 和 set 的參數(shù)傳入進(jìn)去,不必考慮死循環(huán)的問題了。這是直觀的感受。
上面我們使用了 Object.defineProperty()
API 簡(jiǎn)單地實(shí)現(xiàn)了 Vue2 的響應(yīng)式原理,那么 Vue 使用 Proxy 是怎么實(shí)現(xiàn)的呢?它帶來了哪些好處呢?下面我們看實(shí)現(xiàn)源碼:
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) {
// 判斷如果不是一個(gè)對(duì)象的話返回
if (!isObject(target)) return target
// target觀察前的原對(duì)象; proxy觀察后的對(duì)象: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 函數(shù)的實(shí)現(xiàn),我們可以直觀地看到?jīng)]有對(duì) target 進(jìn)行遞歸循環(huán)去創(chuàng)建觀察對(duì)象。而且,當(dāng)我們對(duì) obj 下的 a 屬性設(shè)置值時(shí),執(zhí)行 get 函數(shù),這是為什么呢?這就是 Proxy 的優(yōu)點(diǎn),在對(duì) obj 下屬性設(shè)置值時(shí),首先需要調(diào)用 set 方法獲取 target 下 obj 的值,然后判斷 obj 又是一個(gè)對(duì)象再去調(diào)用 reactive 函數(shù)進(jìn)行觀察。這樣就不需要遞歸地去對(duì)嵌套數(shù)據(jù)進(jìn)行觀察了,而是在獲取值的時(shí)候,判斷獲取的值是不是一個(gè)對(duì)象,這樣極大地節(jié)約了資源。
4. 小結(jié)
本節(jié)主要通過代理和 Object.defineProperty()
API 的學(xué)習(xí)來理解 ES6 的新增知識(shí)點(diǎn) ——Proxy,并且通過 Vue2 和 Vue3 實(shí)現(xiàn)響應(yīng)式原理來對(duì)比 Object.defineProperty()
和 Proxy 的優(yōu)缺點(diǎn),從而更深入地理解 Proxy。