新聞中心
1、寫在前面
在Javascript中,我們知道“萬物皆對(duì)象”,而對(duì)象的實(shí)際語義又是由對(duì)象的內(nèi)部方法來指定的。所謂內(nèi)部方法,指的是在對(duì)一個(gè)對(duì)象進(jìn)行操作時(shí)在引擎內(nèi)部調(diào)用的方法,這些方法對(duì)使用者是不可見的。

創(chuàng)新互聯(lián)堅(jiān)持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:網(wǎng)站制作、成都網(wǎng)站建設(shè)、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時(shí)代的武鳴網(wǎng)站設(shè)計(jì)、移動(dòng)媒體設(shè)計(jì)的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
如何區(qū)分一個(gè)對(duì)象是普通對(duì)象還是函數(shù)呢?
可以通過內(nèi)部方法和內(nèi)部槽來區(qū)分對(duì)象,函數(shù)對(duì)象會(huì)部署方法[[call]],而普通對(duì)象不會(huì)。
2、Proxy的工作原理
當(dāng)然,內(nèi)部方法是具有多態(tài)性的,不同類型的對(duì)象部署相同的內(nèi)部方法,卻有可能有不同的邏輯。
如果在創(chuàng)建代理對(duì)象時(shí)沒有指定對(duì)應(yīng)的攔截方法,那么就會(huì)通過代理對(duì)象訪問屬性值時(shí),代理的內(nèi)部方法(如[[Get]])會(huì)去調(diào)用原始對(duì)象的內(nèi)部方法(如[[Get]])去獲取屬性值,這就會(huì)代理透明。
Proxy也是對(duì)象,在它身上也會(huì)部署許多內(nèi)部方法,當(dāng)我們通過代理對(duì)象去訪問屬性值時(shí),會(huì)調(diào)用部署在代理對(duì)象上的內(nèi)部方法[[Get]]。
Proxy對(duì)象的內(nèi)部方法:
- handler.apply()
- handler.construct()
- handler.defineProperty()
- handler.deleteProperty()
- handler.get()
- handler.getOwnPropertyDescriptor()
- handler.getPrototypeOf()
- handler.has()
- handler.isExtensible()
- handler.ownKeys()
- handler.preventExtensions()
- handler.set()
- handler.setPrototypeOf()
在被代理對(duì)象是函數(shù)時(shí),會(huì)部署另外的兩個(gè)內(nèi)部方法[[Call]]和[[Constructor]]。
當(dāng)我們使用Proxy的deleteProperty()刪除屬性時(shí),實(shí)際上是代理對(duì)象的內(nèi)部方法和行為,改變的只是代理對(duì)象的屬性值。想要改變原始數(shù)據(jù)上的屬性值,必須通過Reflect.deleteProperty(target,key)來實(shí)現(xiàn)。
3、如何代理Object對(duì)象
在前面的文章中,使用get攔截方法對(duì)屬性的讀取操作,其實(shí)是片面的,因?yàn)槭褂胕n操作符檢查對(duì)象的屬性、使用for...in循環(huán)遍歷對(duì)象,都是對(duì)象的讀取操作。
讀取屬性
普通對(duì)象的所有讀取操作:
- 訪問屬性:data.name。
- 判斷對(duì)象或原型上是否存在指定的key:key in data。
- 使用for...in遍歷對(duì)象:for(const key in data){}。
直接訪問屬性
const data = {
name:"pingping"
}
const state = new Proxy(data,{
get(target, key, receiver){
//追蹤函數(shù) 建立副作用函數(shù)與代理對(duì)象的聯(lián)系
track(target, key);
//返回屬性值
Reflect.get(target, key, receiver);
}
})in操作符
const data = {
name:"pingping"
}
const state = new Proxy(data,{
has(target, key, receiver){
//追蹤函數(shù) 建立副作用函數(shù)與代理對(duì)象的聯(lián)系
track(target, key);
//返回屬性值
Reflect.has(target, key, receiver);
}
})for...in
通過攔截ownKeys操作,可以實(shí)現(xiàn)對(duì)for...in循環(huán)的間接攔截,在ownKeys中只能獲取到目標(biāo)對(duì)象target的所有鍵值,但是沒有和具體的鍵綁定。對(duì)此需要使用Symbol構(gòu)造唯一的key值進(jìn)行標(biāo)識(shí),即ITERATE_KEY。
const data = {
name:"pingping"
}
const ITERATE_KEY = Symbol();
const state = new Proxy(data,{
ownKeys(target){
//追蹤函數(shù) 建立副作用函數(shù)與ITERATE_KEY的聯(lián)系
track(target, ITERATE_KEY);
//返回屬性值
Reflect.ownKeys(target);
}
})設(shè)置屬性
如果代理對(duì)象state只有一個(gè)屬性時(shí),for...in循環(huán)只會(huì)執(zhí)行一次,但是當(dāng)state上添加了新的屬性,那么for...in便會(huì)執(zhí)行多次。這是因?yàn)榻o對(duì)象添加新的屬性時(shí),會(huì)觸發(fā)與ITERATE_KEY相關(guān)聯(lián)的副作用函數(shù)重新執(zhí)行。
const data = {
name:"pingping"
}
const ITERATE_KEY = Symbol();
const state = new Proxy(data,{
set(target, key, newVal){
const res = Reflect.set(target, key, newVal, receiver);
trigger(target, key);
return res;
},
ownKeys(target){
//追蹤函數(shù) 建立副作用函數(shù)與ITERATE_KEY的聯(lián)系
track(target, ITERATE_KEY);
//返回屬性值
Reflect.ownKeys(target);
}
})
effect(()=>{
for(const key in state){
console.log(key);//name
}
})trigger函數(shù):
function trigger(target, key){
const depsMap = bucket.get(target);
if(!depsMap) return;
const effects = depsMap.get(key);
const iterateEffects = depsMap.get(ITERATE_KEY);
const effectsToRun = new Set();
// 將與key相關(guān)聯(lián)的副作用函數(shù)添加到effectsToRun中
effect && effects.forEach(effectFn=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
});
// 將與ITERATE_KEY相關(guān)聯(lián)的副作用函數(shù)添加到effectsToRun中
iterateEffects && iterateEffects.forEach(effectFn=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
});
effectsToRun.forEach(effectFn=>{
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
}else{
effectFn();
}
});
}在上面trigger函數(shù)中,在添加屬性時(shí),除了將與key值直接相關(guān)聯(lián)的副作用函數(shù)取出來執(zhí)行外,還需要將那些與ITERATE_KEY相關(guān)聯(lián)的副作用函數(shù)也取出來執(zhí)行。
在上面的代碼中,對(duì)于代理對(duì)象添加新的屬性而言,是可以這樣做的,但是對(duì)于修改現(xiàn)有對(duì)象的現(xiàn)有屬性是不可行的。因?yàn)樵谛薷默F(xiàn)有屬性值,不會(huì)對(duì)for...in循環(huán)造成影響,無論如何修改值都只會(huì)執(zhí)行一次循環(huán)。對(duì)此,不需要觸發(fā)副作用函數(shù)的重新執(zhí)行,否則會(huì)造成額外的性能開銷。
那么,應(yīng)該如何處理呢?
事實(shí)上,無論是在現(xiàn)有對(duì)象新增屬性還是修改現(xiàn)有屬性,都是使用set攔截函數(shù)來實(shí)現(xiàn)攔截的。所以,我們可以將上面代碼片段進(jìn)行整合,在進(jìn)行設(shè)置操作攔截的時(shí)候進(jìn)行判斷,判斷當(dāng)前對(duì)象上是否有該屬性。
- 如果是新增屬性,則多次執(zhí)行觸發(fā)ITERATE_KEY相關(guān)聯(lián)的副作用函數(shù)執(zhí)行。
- 如果是修改屬性,則不需要觸發(fā)ITERATE_KEY相關(guān)聯(lián)的副作用函數(shù)執(zhí)行。
const TriggerType = {
SET:"SET",
ADD:"ADD"
};
const state = new Proxy(data,{
set(target, key, newVal){
const type = Object.prototype.hasOwnProperty.call(target,key) ? TriggerType.SET : TriggerType.ADD;
const res = Reflect.set(target, key, newVal, receiver);
// 傳入判斷當(dāng)前是否新增屬性
trigger(target, key, type);
return res;
}
})
function trigger(target, key, type){
const depsMap = bucket.get(target);
if(!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set();
// 將與key相關(guān)聯(lián)的副作用函數(shù)添加到effectsToRun中
effect && effects.forEach(effectFn=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
});
if(type === TriggerType.ADD){
const iterateEffects = depsMap.get(ITERATE_KEY);
// 將與ITERATE_KEY相關(guān)聯(lián)的副作用函數(shù)添加到effectsToRun中
iterateEffects && iterateEffects.forEach(effectFn=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
});
}
effectsToRun.forEach(effectFn=>{
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
}else{
effectFn();
}
});
}刪除屬性
在代理對(duì)象商,刪除屬性可以通過delete進(jìn)行刪除,那么delete操作符依賴Proxy對(duì)象內(nèi)部方法deleteProperty。同樣的,在刪除指定屬性時(shí),需要先檢查當(dāng)前屬性是否在對(duì)象自身上,然后再考慮Reflect.deleteProperty函數(shù)完成屬性的刪除。
既然是操作代理對(duì)象的屬性刪除,那么就會(huì)觸發(fā)trigger的依賴收集操作,副作用函數(shù)會(huì)重新執(zhí)行。對(duì)象屬性的數(shù)目變少,那么就會(huì)影響for...in循環(huán)的次數(shù),會(huì)觸發(fā)與ITERATE_KEY相關(guān)聯(lián)的副作用函數(shù)的重新執(zhí)行。
const TriggerType = {
SET:"SET",
ADD:"ADD",
DELETE:"DELETE"
};
const state = new Proxy(data, {
deleteProperty(target, key){
// 檢查當(dāng)前要?jiǎng)h除的屬性是否在對(duì)象上
const hadKey = Object.property.hasOwnProperty.call(target, key);
// 使用`Reflect.deleteProperty`函數(shù)完成屬性的刪除
const res = Reflect.deleteProperty(target, key);
if(res && hadKey){
//只有刪除成功才會(huì)觸發(fā)更新
trigger(target, key, "DELETE");
}
}
})
function trigger(target, key, type){
const depsMap = bucket.get(target);
if(!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set();
// 將與key相關(guān)聯(lián)的副作用函數(shù)添加到effectsToRun中
effect && effects.forEach(effectFn=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
});
if(type === TriggerType.ADD || type === TriggerType.DELETE){
const iterateEffects = depsMap.get(ITERATE_KEY);
// 將與ITERATE_KEY相關(guān)聯(lián)的副作用函數(shù)添加到effectsToRun中
iterateEffects && iterateEffects.forEach(effectFn=>{
if(effectFn !== activeEffect){
effectsToRun.add(effectFn);
}
});
}
effectsToRun.forEach(effectFn=>{
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
}else{
effectFn();
}
});
}4、合理觸發(fā)響應(yīng)
在前面的文字中,從規(guī)范的角度詳細(xì)地介紹了如何實(shí)現(xiàn)對(duì)象代理,與此同時(shí),處理了很多邊界條件。需要明確知道操作類型才能觸發(fā)響應(yīng),但是在觸發(fā)響應(yīng)時(shí)也要看是否合理,在值沒有發(fā)生變化時(shí)就不需要觸發(fā)響應(yīng)。
對(duì)此,在修改set攔截函數(shù)的代碼時(shí),在調(diào)用trigger函數(shù)觸發(fā)響應(yīng)前,需要檢查值是否發(fā)生真實(shí)改變。
const data = {
name:"pingping"
};
const state = new Proxy(data,{
set(target, key, newVal, receiver){
// 先獲取舊值
const oldVal = target[key];
const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD";
const res = Reflect.set(target, key, newVal, receiver);
if(oldVal !== newVal){
trigger(target, key, type);
}
return res
})
effect(()=>{
console.log(state.name);
});
state.name = "onechuan";在調(diào)用set攔截函數(shù)時(shí),需要先獲取oldVal與新值newVal進(jìn)行比較,只有二者不全等的時(shí)候才會(huì)觸發(fā)響應(yīng)。當(dāng)時(shí),當(dāng)oldVal和newVal的值都為NaN時(shí),使用全等進(jìn)行比較得到的是false。
NaN === NaN //false
NaN !== NaN //true
我們看到NaN值的比較值,當(dāng)data.num的初始值為NaN時(shí),后續(xù)修改其值為NaN作為新值,此時(shí)還是使用全等比較,得到NaN !== NaN值為true,就會(huì)觸發(fā)響應(yīng)函數(shù),導(dǎo)致不必要的更新。對(duì)此需要先判斷oldVal和newVal的值都不為NaN,那么需要加上判斷oldVal === oldVal || newVal === newVal,其實(shí)等價(jià)于Number.isNaN(newVal) || Number.isNaN(oldVal)。
為了方便使用,我們對(duì)對(duì)象的代理進(jìn)行函數(shù)封裝。
function reactive(){
return new Proxy(data,{
set(target, key, newVal, receiver){
// 先獲取舊值
const oldVal = target[key];
const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD";
const res = Reflect.set(target, key, newVal, receiver);
if(oldVal !== newVal && (oldVal === oldVal || newVal === newVal)){
trigger(target, key, type);
}
return res
})
}這樣,在使用時(shí):
const obj = {};
const data = {
name:"pingping"
}
const parent = reactive(data);
const child = reactive(obj);
//使用parent對(duì)象作為child的原型對(duì)象
Object.setPrototypeOf(child, parent);
effect(()=>{
console.log(child.name);//pingping
});
//修改了child.name的值
child.name = "onechuan";//會(huì)導(dǎo)致副作用函數(shù)重新執(zhí)行兩次在上面的代碼中,會(huì)導(dǎo)致副作用函數(shù)重新執(zhí)行兩次。其實(shí)做的處理就是分別使用Proxy對(duì)obj和data進(jìn)行代理,并將parent對(duì)象作為child的原型對(duì)象。在副作用函數(shù)中讀取child.name的值時(shí),會(huì)觸發(fā)child代理對(duì)象的get攔截函數(shù),而攔截函數(shù)的實(shí)現(xiàn)是Reflect.get(obj, "name", receiver)。
但是呢,child對(duì)象本身上本不存在name屬性,對(duì)此就會(huì)去獲取對(duì)象的原型parent并調(diào)用原型的[[Get]]方法得到結(jié)果parent.name的值。而parent本身又是響應(yīng)式數(shù)據(jù),對(duì)此在副作用函數(shù)中訪問parent.name的值,會(huì)導(dǎo)致副作用函數(shù)被收集并建立響應(yīng)聯(lián)系。parent.name和child.name都會(huì)觸發(fā)副作用函數(shù)的依賴收集,即都與副作用函數(shù)建立了聯(lián)系。
重新分析下上面的代碼,當(dāng)child.name = 2被執(zhí)行時(shí),會(huì)調(diào)用child對(duì)象的set攔截函數(shù),而在set攔截函數(shù)內(nèi)部實(shí)現(xiàn)是Reflect.get(target, key, newVale, receiver)完成默認(rèn)設(shè)置行為。由于child和其所代理的對(duì)象obj上沒有name屬性,則會(huì)去原型parent上進(jìn)行尋找,即導(dǎo)致parent代理對(duì)象的set攔截函數(shù)被執(zhí)行。
而在讀取child.name的值時(shí),副作用函數(shù)不僅會(huì)被child.name觸發(fā)執(zhí)行,還會(huì)被parent.name所收集,對(duì)此在parent代理對(duì)象的set攔截函數(shù)被執(zhí)行時(shí),會(huì)觸發(fā)副作用函數(shù)重新執(zhí)行。對(duì)此,副作用函數(shù)被執(zhí)行了兩次。
那么,應(yīng)該如何避免執(zhí)行兩次副作用函數(shù)呢?
其實(shí),我們需要區(qū)分兩次副作用函數(shù)執(zhí)行是誰觸發(fā)的,其實(shí)只需要確定recevier是不是target的代理對(duì)象,然后將parent.name觸發(fā)的副作用函數(shù)執(zhí)行進(jìn)行屏蔽即可。
function reactive(){
return new Proxy(data,{
get(target, key, receiver){
// 代理對(duì)象可以通過raw屬性訪問數(shù)據(jù)
if(key === "raw"){
return target
}
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, newVal, receiver){
// 先獲取舊值
const oldVal = target[key];
const type = Object.prototype.hasOwnProperty.call(target, key) ? "SET" : "ADD";
const res = Reflect.set(target, key, newVal, receiver);
// target === receiver.raw可以說明receiver是target的代理對(duì)象
if(target === receiver.raw){
if(oldVal !== newVal && (oldVal === oldVal || newVal === newVal)){
trigger(target, key, type);
}
}
return res
})
}在上面代碼中,我們新增判斷條件target === receiver.raw,只有的那個(gè)其為true,即recevier是target的代理對(duì)象時(shí)觸發(fā)更新,就可以屏蔽由于原型引起的更新,從而避免不必要的更新操作。
5、寫在最后
上篇文章中介紹了好哥們Proxy和Reflect的作用,這篇文章介紹了Proxy如何實(shí)現(xiàn)對(duì)Object對(duì)象的代理,分別對(duì)代理對(duì)象的設(shè)值、取值、刪除屬性等操作進(jìn)行了介紹。還討論了,如何合理觸發(fā)副作用函數(shù)重新執(zhí)行,以及屏蔽由原型更新引起的副作用函數(shù)不必要的重新執(zhí)行。
網(wǎng)站標(biāo)題:Vue.js設(shè)計(jì)與實(shí)現(xiàn)之九-Object對(duì)象類型的響應(yīng)式代理
分享URL:http://fisionsoft.com.cn/article/cceogeh.html


咨詢
建站咨詢
