從 Proxy 說起
什么是Proxy
proxy翻譯過來的意思就是”代理“老虫,ES6對(duì)Proxy的定位就是target對(duì)象(原對(duì)象)的基礎(chǔ)上通過handler增加一層”攔截“祈匙,返回一個(gè)新的代理對(duì)象,之后所有在Proxy中被攔截的屬性,都可以定制化一些新的流程在上面洁闰,先看一個(gè)最簡(jiǎn)單的例子
const target = {}; // 要被代理的原對(duì)象
// 用于描述代理過程的handler
const handler = {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}}
// obj就是一個(gè)被新的代理對(duì)象
const obj = new Proxy(target, handler);
obj.a = 1 // setting a!
console.log(obj.a)// getting a!
上面的例子中我們?cè)趖arget對(duì)象上架設(shè)了一層handler扑眉,其中攔截了針對(duì)target的get和set腰素,然后我們就可以在get和set中間做一些額外的操作了
注意1:對(duì)Proxy對(duì)象的賦值操作也會(huì)影響到原對(duì)象target弓千,同時(shí)對(duì)target的操作也會(huì)影響Proxy献起,不過直接操作原對(duì)象的話不會(huì)觸發(fā)攔截的內(nèi)容~
obj.a = 1; // setting a!
console.log(target.a) // 1 不會(huì)打印 "getting a!"
注意2:如果handler中沒有任何攔截上的處理,那么對(duì)代理對(duì)象的操作會(huì)直接通向原對(duì)象
const target = {};
const handler = {};
const obj = new Proxy(target, handler);
obj.a = 1;
console.log(target.a) // 1
既然proxy也是一個(gè)對(duì)象呆抑,那么它就可以做為原型對(duì)象汁展,所以我們把obj的原型指向到proxy上后食绿,發(fā)現(xiàn)對(duì)obj的操作會(huì)找到原型上的代理對(duì)象炫欺,如果obj自己有a屬性,則不會(huì)觸發(fā)proxy上的get树姨,這個(gè)應(yīng)該很好理解
const target = {};
const obj = {};
const handler = {
get: function(target, key){
console.log(`get ${key} from ${JSON.stringify(target)}`);
return Reflect.get(target, key);
}}
const proxy = new Proxy(target, handler);
Object.setPrototypeOf(obj, proxy);
proxy.a = 1;
obj.b = 1
console.log(obj.a) // get a from {"a": 1} 1
console.log(obj.b) // 1
ES6的Proxy實(shí)現(xiàn)了對(duì)哪些屬性的攔截帽揪?
通過上面的例子了解了Proxy的原理后转晰,我們來看下ES6目前實(shí)現(xiàn)了哪些屬性的攔截查邢,以及他們分別可以做什么扰藕? 下面是 Proxy 支持的攔截操作一覽芳撒,一共 13 種
- get(target, propKey, receiver):攔截對(duì)象屬性的讀取笔刹,比如proxy.foo和proxy['foo'];
- set(target, propKey, value, receiver):攔截對(duì)象屬性的設(shè)置舌菜,比如proxy.foo = v或proxy['foo'] = v,返回一個(gè)布爾值;
- has(target, propKey):攔截propKey in proxy的操作染乌,返回一個(gè)布爾值懂讯。
- deleteProperty(target, propKey):攔截delete proxy[propKey]的操作褐望,返回一個(gè)布爾值;
- ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、
Object.getOwnPropertySymbols(proxy)实蔽、Object.keys(proxy)局装、for…in循環(huán)铐尚,返回一個(gè)數(shù)組宣增。該方法返回目標(biāo)對(duì)象所有自身的屬性的屬性名矛缨,而Object.keys()的返回結(jié)果僅包括目標(biāo)對(duì)象自身的可遍歷屬性;- getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey)箕昭,返回屬性的描述對(duì)象;
- defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)落竹、Object.defineProperties(proxy, propDescs),返回一個(gè)布爾值;
- preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個(gè)布爾值;
- getPrototypeOf(target):攔截Object.getPrototypeOf(proxy)桨武,返回一個(gè)對(duì)象;
- isExtensible(target):攔截Object.isExtensible(proxy)呀酸,返回一個(gè)布爾值;
- setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto)琼梆,返回一個(gè)布爾值。如果目標(biāo)對(duì)象是函數(shù)错览,那么還有兩種額外操作可以攔截;
- apply(target, object, args):攔截 Proxy 實(shí)例作為函數(shù)調(diào)用的操作倾哺,比如proxy(…args)、proxy.call(object, …args)忌愚、proxy.apply(…);
- construct(target, args):攔截 Proxy 實(shí)例作為構(gòu)造函數(shù)調(diào)用的操作硕糊,比如new proxy(…args);
以上是目前es6支持的proxy简十,具體的用法不做贅述勺远,有興趣的可以到阮一峰老師的es6入門去研究每種的具體用法时鸵,其實(shí)思想都是一樣的饰潜,只是每種對(duì)應(yīng)了一些不同的功能~
實(shí)際場(chǎng)景中 Proxy 可以做什么彭雾?
實(shí)現(xiàn)私有變量
js的語法中沒有private這個(gè)關(guān)鍵字來修飾私有變量,所以基本上所有的class的屬性都是可以被訪問的半沽,但是在有些場(chǎng)景下我們需要使用到私有變量者填,現(xiàn)在業(yè)界的一些做法都是使用”_變量名“來”約定“這是一個(gè)私有變量占哟,但是如果哪天被別人從外部改掉的話榨乎,我們還是沒有辦法阻止的,然而铐姚,當(dāng)Proxy出現(xiàn)后谦屑,我們可以用代理來處理這種場(chǎng)景氢橙,看代碼:
const obj = {
_name: 'nanjin',
age: 19,
getName: () => {
return this._name;
},
setName: (newName) => {
this._name = newName;
}}
const proxyObj = obj => new Proxy(obj, {
get: (target, key) => {
if(key.startsWith('_')){
throw new Error(`${key} is private key, please use get${key}`)
}
return Reflect.get(target, key);
},
set: (target, key, newVal) => {
if(key.startsWith('_')){
throw new Error(`${key} is private key, please use set${key}`)
}
return Reflect.set(target, key, newVal);
}})
const newObj = proxyObj(obj);
console.log(newObj._name) // Uncaught Error: _name is private key, please use get_name
newObj._name = 'newname'; // Uncaught Error: _name is private key, please use set_name
console.log(newObj.age) // 19
console.log(newObj.getName()) // nanjin
可見悍手,通過proxyObj方法坦康,我們可以實(shí)現(xiàn)把任何一個(gè)對(duì)象都過濾一次诡延,然后返回新的代理對(duì)象肆良,被處理的對(duì)象會(huì)把所有_開頭的變量給攔截掉,更進(jìn)一步夭谤,如果有用過mobx的同學(xué)會(huì)發(fā)現(xiàn)mobx里面的store中的對(duì)象都是類似于這樣的
有handler 和 target朗儒,說明mobx本身也是用了代理模式醉锄,同時(shí)加上Decorator函數(shù)恳不,在這里就相當(dāng)于把proxyObj使用裝飾器的方式來實(shí)現(xiàn),Proxy + Decorator 就是mobx的核心原理啦~
vue響應(yīng)式數(shù)據(jù)實(shí)現(xiàn)
VUE的雙向綁定涉及到模板編譯,響應(yīng)式數(shù)據(jù)神妹,訂閱者模式等等鸵荠,有興趣的可以看這里蛹找,因?yàn)檫@篇文章的主題是proxy哨坪,因此我們著重介紹一下數(shù)據(jù)響應(yīng)式的過程当编。
2.x版本
在當(dāng)前的vue2.x的版本中,在data中聲名一個(gè)obj后金顿,vue會(huì)利用Object.defineProperty來遞歸的給data中的數(shù)據(jù)加上get和set揍拆,然后每次set的時(shí)候嫂拴,加入額外的邏輯慧妄。來觸發(fā)對(duì)應(yīng)模板視圖的更新塞淹,看下偽代碼:
const defineReactiveData = data => {
Object.keys(data).forEach(key => {
let value = data[key];
Object.defineProperty(data, key, {
get : function(){
console.log(`getting ${key}`)
return value;
},
set : function(newValue){
console.log(`setting ${key}`)
notify() // 通知相關(guān)的模板進(jìn)行編譯
value = newValue;
},
enumerable : true,
configurable : true
})
})}
這個(gè)方法可以給data上面的所有屬性都加上get和set饱普,當(dāng)然這只是偽代碼套耕,實(shí)際場(chǎng)景下我們還需要考慮如果某個(gè)屬性還是對(duì)象我們應(yīng)該遞歸下去,來試試:
const data = {
name: 'nanjing',
age: 19
}
defineReactiveData(data)
data.name // getting name 'nanjing'
data.name = 'beijing'; // setting name
可以看到當(dāng)我們get和set觸發(fā)的時(shí)候匈挖,已經(jīng)能夠同時(shí)觸發(fā)我們想要調(diào)用的函數(shù)拉,Vue雙向綁定過程中舶吗,當(dāng)改變this上的data的時(shí)候去更新模板的核心原理就是這個(gè)方法誓琼,通過它我們就能在data的某個(gè)屬性被set的時(shí)候腹侣,去觸發(fā)對(duì)應(yīng)模板的更新齿穗。
現(xiàn)在我們?cè)趤碓囋囅旅娴拇a:
const data = {
userIds: ['01','02','03','04','05']
}
defineReactiveData(data);
data.userIds // getting userIds ["01", "02", "03", "04", "05"]
// get 過程是沒有問題的缤灵,現(xiàn)在我們嘗試給數(shù)組中push一個(gè)數(shù)據(jù)
data.userIds.push('06') // getting userIds
what ? setting沒有被觸發(fā)腮出,反而因?yàn)槿×艘淮蝩serIds所以觸發(fā)了一次getting~,
不僅如此作儿,很多數(shù)組的方法都不會(huì)觸發(fā)setting攻锰,比如:push,pop,shift,unshift,splice,sort,reverse這些方法都會(huì)改變數(shù)組娶吞,但是不會(huì)觸發(fā)set械姻,所以Vue為了解決這個(gè)問題楷拳,重新包裝了這些函數(shù),同時(shí)當(dāng)這些方法被調(diào)用的時(shí)候陶耍,手動(dòng)去觸發(fā)notify()烈钞;看下源碼:
// 獲得數(shù)組原型const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重寫以下函數(shù)const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function(method) {
// 緩存原生函數(shù)
const original = arrayProto[method]
// 重寫函數(shù)
def(arrayMethods, method, function mutator(...args) {
// 先調(diào)用原生函數(shù)獲得結(jié)果
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
// 調(diào)用以下幾個(gè)函數(shù)時(shí),監(jiān)聽新數(shù)據(jù)
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 手動(dòng)派發(fā)更新
ob.dep.notify()
return result
})
})
上面是官方的源碼蛾狗,我們可以實(shí)現(xiàn)一下push的偽代碼,為了省事算吩,直接在prototype上下手了~
const push = Array.prototype.push;
Array.prototype.push = function(...args){
console.log('push is happenning');
return push.apply(this, args);
}
data.userIds.push('123') // push is happenning
通過這種方式偎巢,我們可以監(jiān)聽到這些的變化兼耀,但是vue官方文檔中有這么一個(gè)注意事項(xiàng)
由于 JavaScript 的限制瘤运,Vue 不能檢測(cè)以下變動(dòng)的數(shù)組:
- 當(dāng)你利用索引直接設(shè)置一個(gè)項(xiàng)時(shí),例如:vm.items[indexOfItem] = newValue
- 當(dāng)你修改數(shù)組的長(zhǎng)度時(shí)但金,例如:vm.items.length = newLength
這個(gè)最根本的原因是因?yàn)檫@2種情況下冷溃,受制于js本身無法實(shí)現(xiàn)監(jiān)聽似枕,所以官方建議用他們自己提供的內(nèi)置api來實(shí)現(xiàn)年柠,我們也可以理解到這里既不是defineProperty可以處理的彪杉,也不是包一層函數(shù)就能解決的,這就是2.x版本現(xiàn)在的一個(gè)問攀唯。
回到這篇文章的主題侯嘀,vue官方會(huì)在3.x的版本中使用proxy來代替defineProperty處理響應(yīng)式數(shù)據(jù)的過程,我們先來模擬一下實(shí)現(xiàn)吠谢,看看能否解決當(dāng)前遇到的這些問題工坊;
3.x版本
我們先來通過proxy實(shí)現(xiàn)對(duì)data對(duì)象的get和set的劫持敢订,并返回一個(gè)代理的對(duì)象,注意昭齐,我們只關(guān)注proxy本身矾柜,所有的實(shí)現(xiàn)都是偽代碼怪蔑,有興趣的同學(xué)可以自行完善
const defineReactiveProxyData = data => new Proxy(data,{
get: function(data, key){
console.log(`getting ${key}`)
return Reflect.get(data, key);
},
set: function(data, key, newVal){
console.log(`setting ${key}`);
if(typeof newVal === 'object'){ // 如果是object缆瓣,遞歸設(shè)置代理
return Reflect.set(data, key, defineReactiveProxyData(newVal));
}
return Reflect.set(data, key, newVal);
}
})
const data = {
name: 'nanjing',
age: 19
};
const vm = defineReactiveProxyData(data);
vm.name // getting name nanjing
vm.age = 20; // setting age 20
看起來我們的代理已經(jīng)起作用啦捆愁,之后只要在setting的時(shí)候加上notify()去通知模板進(jìn)行編譯就可以了,然后我們來嘗試設(shè)置一個(gè)數(shù)組看看呻逆;
vm.userIds = [1,2,3] // setting userIds
vm.userIds.push(1);
// getting userIds 因?yàn)槲覀儠?huì)先訪問一次userids
// getting push 調(diào)用了push方法咖城,所以會(huì)訪問一次push屬性
// getting length 數(shù)組push的時(shí)候 length會(huì)變宜雀,所以需要先訪問原來的length
// setting 3 通過下標(biāo)設(shè)置的握础,所以set當(dāng)前的index是3
// setting length 改變了數(shù)組的長(zhǎng)度禀综,所以會(huì)set length
// 4 返回新的數(shù)組的長(zhǎng)度
回顧2.x遇到的第一個(gè)問題苔严,需要重新包裝Array.prototype上的一些方法届氢,使用了proxy后不需要了退子,解決了~絮供,繼續(xù)看下一個(gè)問題
vm.userIds.length = 2
// getting userIds 先訪問
// setting length 在設(shè)置
vm.userIds[1] = '123'
// getting userIds 先訪問
// setting 1 設(shè)置index=1的item
// "123"
從上面的例子中我們可以看到茶敏,不管是直接改變數(shù)組的length還是通過某一個(gè)下標(biāo)改變數(shù)組的內(nèi)容惊搏,proxy都能攔截到這次變化恬惯,這比defineProperty方便太多了酪耳,2.x版本中的第二個(gè)問題刹缝,在proxy中根本不會(huì)出現(xiàn)了梢夯。
總結(jié)1
通過上面的例子和代碼,我們看到Vue的響應(yīng)模式如果使用proxy會(huì)比現(xiàn)在的實(shí)現(xiàn)方式要簡(jiǎn)化和優(yōu)化很多噪奄,很快在即將來臨的3.0版本中勤篮,大家就可以體驗(yàn)到了色罚。不過因?yàn)閜roxy本身是有兼容性的戳护,比如ie瀏覽器涤垫,所以在低版本的場(chǎng)景下蝠猬,vue會(huì)回退到現(xiàn)在的實(shí)現(xiàn)方式榆芦。
總結(jié)2
回歸到proxy本身喘鸟,設(shè)計(jì)模式中有一種典型的代理模式,proxy就是js的一種實(shí)現(xiàn)崎淳,它的好處在于拣凹,我可以在不污染本身對(duì)象的條件下恨豁,生成一個(gè)新的代理對(duì)象,所有的一些針對(duì)性邏輯放到代理對(duì)象上去實(shí)現(xiàn)菊匿,這樣我可以由A對(duì)象计福,衍生出B,C,D…每個(gè)的處理過程都不一樣象颖,從而簡(jiǎn)化代碼的復(fù)雜性,提升一定的可讀性可款,比如用proxy實(shí)現(xiàn)數(shù)據(jù)庫(kù)的ORM就是一種很好的應(yīng)用闺鲸,其實(shí)代碼很簡(jiǎn)單埃叭,關(guān)鍵是要理解背后的思想,同時(shí)能夠舉一反三~
擴(kuò)展:
1.Proxy.revocable()
這個(gè)方法可以返回一個(gè)可取消的代理對(duì)象
const obj = {};
const handler = {};
const {proxy, revoke} = Proxy.revocable(obj, handler);
proxy.a = 1
proxy.a // 1
revoke();
proxy.a // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
一旦代理被取消了壁袄,就不能再?gòu)拇韺?duì)象訪問了
打印proxy 可以看到IsRevoked變?yōu)閠rue了
2.代理對(duì)象的this問題
因?yàn)閚ew Proxy出來的是一個(gè)新的對(duì)象嗜逻,所以在如果你在target中有使用this缭召,被代理后的this將指向新的代理對(duì)象嵌巷,而不是原來的對(duì)象搪哪,這個(gè)時(shí)候,如果有些函數(shù)是原對(duì)象獨(dú)有的惑朦,就會(huì)出現(xiàn)this指向?qū)е碌膯栴}已维,這種場(chǎng)景下垛耳,建議使用bind來強(qiáng)制綁定this
看代碼:
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate(); // Uncaught TypeError: this is not a Date object.
因?yàn)榇砗蟮膶?duì)象并不是一個(gè)Date類型的堂鲜,不具有g(shù)etDate方法的护奈,所以我們需要在get的時(shí)候霉旗,綁定一下this的指向
const target = new Date();
const handler = {
get: function(target, key){
if(typeof target[key] === 'function'){
return target[key].bind(target) // 強(qiáng)制綁定
this到原對(duì)象
}
return Reflect.get(target, key)
}
};
const proxy = new Proxy(target, handler);
proxy.getDate(); // 6
這樣就可以正常使用this啦厌秒,當(dāng)然具體的使用還要看具體的場(chǎng)景,靈活運(yùn)用吧檐晕!
作者:Guokai
鏈接:https://juejin.im/post/5cf8b51ae51d45590a445b0d
求點(diǎn)贊辟灰,求關(guān)注~