Vue.js設計與實現之十-原始類型的響應式代理
1、寫在前面
在javascript中原始值包括:Boolean、String、Number、Null、Undefined、Symbol和BigInt等類型,原始值是按值傳遞而非按引用傳遞。前面,知道Proxy可以用于實現對象類型的響應式代理,但是卻不能實現原始值的代理,要實現原始值變成響應式數據,就需要做些處理。
2、ref
Proxy的代理目標必須是對象類型,那么是否可以將原始值類型包裝成對象類型,這樣不就可以實現代理了嗎?
// let name = "pingping"
const data = {
value: "pingping"
}
const state = reactive(data);
name.value = "onechuan";
想法是很好,但是你想過沒有這樣做帶來的問題:
- 用戶創建一個原始值的響應式數據,就必須創建一個包裹的對象。
- 而包裹對象又是由用戶自定義,那么就存在命名和使用不規范情況。
解決方法很簡單,你不是擔心用戶自定義的對象不規范不可控嗎,那么就在源碼內部定義不就行了。
function ref(val){
const wrapper = {
value: val
}
return reactive(wrapper);
}
簡單試用下:
const refVal = ref("pingping");
effect(()=>{
console.log(refVal.value);
});
refVal.value = "onechuan";
但是,在使用過程中又有個問題:你又是如何保證refVal是原始值的包裹對象,還是一個非原始值的響應式數據呢?
const refVal = ref("pingping");
const refVal2 = reactive({value:"pingping"});
其實,ref和reactive生成的響應式數據實現方式都是一樣的,對數據來源區分是不是ref是為了后續脫ref,脫出響應式能力恢復原始數據。
function ref(val){
const wrapper = {
value: val
}
Object.defineProperty(wrapper,"__v_isRef",{
value: true
})
return reactive(wrapper);
}
在上面代碼中,使用Object.defineProperty給包裹對象wrapper定義一個不可枚舉和不可寫的屬性"__v_isRef",使其值為true用于區分當前對象是ref而非普通對象。
簡而言之:ref其實是對一個對象和reactive的二次封裝。
3、響應丟失的問題
我們知道,ref可以用于實現原始值的響應式代理,但其實還可以用于解決響應式丟失的問題。所謂響應式丟失,就是在使用reactive生成的響應式對象數據,使用展開運算符(...)會丟失響應式,就成了一個普通對象數據。此時,修改修改對象的屬性值,不會觸發更新和模板渲染。
const obj = reactive({name:"pingping"});
const newObj = {...obj};
effect(()=>{
console.log(newObj.name);
});
obj.nmae = "onechuan";
在上面代碼中,副作用函數中訪問的只是普通對象newObj的屬性name的值,它并不具有響應式能力,在對其屬性值進行修改時,不會觸發副作用函數重新執行。
那么,應該如何解決響應式丟失的問題呢?
其實就是能解決在副作用函數中,通過獲取普通對象newObj的屬性值,也會觸發更新,與副作用函數建立聯系。
通過在普通對象newObj中設置與obj對象同名的屬性,將每個屬性值都設置成對象,通過對象的get取值方法實現obj對象的屬性值讀取,這樣就巧妙地將newObj的屬性值與副作用函數建立了聯系。
const obj = reactive({name:"pingping"});
const newObj = {...obj};
effect(()=>{
console.log(newObj.name);
});
obj.nmae = "onechuan";
但是,如果obj對象中有很多屬性,那是不是就需要在newObj建立許多同名的對象?那么,就可以進行抽取封裝函數:
function toRef(obj, key){
const wrapper = {
get value(){
return obj[key];
},
set value(val){
obj[key] = val
}
}
Object.defineProperty(wrapper,"__v_isRef",{
value: true
})
return wrapper;
}
在使用過程中,簡簡單單:
const obj = reactive({name:"pingping"});
const name = toRef(obj, "name");
name.value = "onechuan";
前面只是對少數對象的屬性值轉成響應式數據可以這樣處理,但是當我們需要批量處理數據,應該如何處理呢?
很簡單,對對象屬性進行遍歷不就得了。
function toRefs(obj){
const res = {};
for(const key in obj){
res[key] = toRef(obj,key);
}
return res;
}
這樣,響應式丟失問題就被解決了,方法就是將響應式數據轉換成類似ref結構的數據,通過toRef或toRefs轉換后得到的數據就是真正的ref數據。
4、自動脫ref
使用toRefs用于解決響應丟失問題,就是對對象的屬性進行遍歷轉為ref,這樣就會帶來新問題,就是去訪問數據的第一層屬性,必須通過.value才能訪問。這樣無疑會增加使用者的心智負擔,用戶肯定愿意直接對象.屬性,而非通過對象.屬性.value來使用屬性值。
const obj = reactive({
name:"pingping",
age:18
});
const newObj = {
...toRefs(obj)
};
newObj.name.value//pingping
newObj.age.value//18
現在我們就需要讓其自動脫ref,這樣在進行對象屬性的訪問時,讀取到屬性是個ref則放回ref.value,否則直接返回屬性值。
function proxyRefs(target){
return new Proxy(target,{
get(target, key, receiver){
const value = Reflect.get(target, key, receiver);
return value.__v_isRef ? value.value : value;
}
})
}
const newObj = proxyRefs(...toRefs(obj));
在上面代碼中,通過定義一個proxyRefs函數接收一個對象參數,返回該對象的代理對象。而代理對象的作用是通過get操作,在讀取到對象的屬性是個ref值時,直接返回該ref.value值,否則直接返回屬性值,這樣就實現了自動脫ref。
其實,在模板中使用ref的屬性值時,就是通過將組件setup返回的數據傳遞到proxyRefs函數中進行處理。這樣就可以實現,在模板中直接訪問屬性值,而非屬性.value值。
前面有實現自動脫ref的能力,現在就有實現自動穿ref的能力。實現原理,同樣的是通過添加對應的set攔截函數。
function proxyRefs(target){
return new Proxy(target,{
get(target, key, receiver){
const value = Reflect.get(target, key, receiver);
return value.__v_isRef ? value.value : value;
},
set(target, key, newValue, receiver){
const value = target[key];
if(value.__v_isRef){
value.value = newValue;
return true
}
return Reflect.set(target, key, newValue, receiver);
}
})
}
5、寫在最后
在本文中主要介紹了如何將原始值轉為響應式數據,如何解決響應式丟失的問題,如何減少用戶心智負擔實現自動脫ref的能力等。ref本質就是一個包裹對象,通過reactive實現對原始值的響應式代理,但是包裹對象自愛本質上又和普通對象沒啥區別,對此需要通過設置一個標識符__v_isRef來實現ref數據的區分。