Vue reactive函數(shù)實(shí)現(xiàn)流程詳解
1.Reflect
Proxy有著可以攔截對(duì)對(duì)象各種操作的能力,比如最基本的get和set操作,而Reflect也有與這些操作同名的方法,像Reflect.set()、Reflect.get(),這些方法和它們所對(duì)應(yīng)的對(duì)象基本操作完全一致。
const data = {
value: '1',
get fn() {
console.log(this.value);
return this.value;
}
};
data.value; // 1
Reflect.get(data,'value'); // 1 除此之外,Reflect除了和基本對(duì)象操作等價(jià)外,它還具有第三個(gè)參數(shù)receiver,即指定該基礎(chǔ)操作的this對(duì)象。
Reflect.get(data,'value',{<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->value: '2'}); // 會(huì)輸出2對(duì)于Proxy,它只能夠攔截對(duì)象的基本操作,而對(duì)于data.fn(),這是一個(gè)復(fù)合操作,它由一個(gè)get操作和一個(gè)apply操作組成,即先通過get獲取fn的值,然后調(diào)用即apply對(duì)應(yīng)的函數(shù)。而現(xiàn)在,用我們之前創(chuàng)建的響應(yīng)式系統(tǒng)來執(zhí)行一次這個(gè)復(fù)合操作,我們期望的結(jié)果是,在對(duì)fn屬性綁定的同時(shí),對(duì)value的值也進(jìn)行綁定,因?yàn)樵趂n函數(shù)的執(zhí)行過程中,操作了value值??蓪?shí)際情況是,value的值并沒有進(jìn)行綁定。
effect(() => {
obj.fn(); // 假設(shè)obj是一個(gè)已經(jīng)做了響應(yīng)式代理的Proxy對(duì)象
})
obj.value = '2'; // 改變obj.value的值,預(yù)想中的響應(yīng)式操作沒有執(zhí)行
這里就涉及到fn()函數(shù)中,this指向的問題了。實(shí)際上,在fn函數(shù)中,this指向的是原來的data對(duì)象,即this.value實(shí)際上是data.value,因?yàn)椴僮鞯氖窃瓕?duì)象,因此并不會(huì)觸依賴收集。了解到問題的原因之后,我們就可以用上之前所說的Reflect的特性了,將get操作實(shí)際的this對(duì)象指定為obj,這樣就可以順利的實(shí)現(xiàn)我們我期望的功能了。
const obj = new Proxy(data, {
get(target, key, receiver) { // get 接收第三個(gè)參數(shù),即操作的調(diào)用者,對(duì)應(yīng)obj.fn()就是obj了
track(target, key);
return Reflect.get(target, key, receiver); // 將原來直接返回target[key]的操作改為Reflect.get
}
}
2.Proxy的工作原理
在js中一個(gè)對(duì)象必須部署包括[[GET]]、[[SET]]在內(nèi)的11個(gè)內(nèi)部方法,除此之外,函數(shù)擁有額外的[[Call]]和[[Construct]]兩個(gè)方法。而在創(chuàng)建Proxy對(duì)象時(shí),指定的攔截函數(shù),實(shí)際上就是用來自定義代理對(duì)象本身的內(nèi)部方法和行為,而不是指定。
3.代理Object
(1)代理讀取操作
對(duì)一個(gè)普通對(duì)象的所有可能的讀取操作:
- 訪問屬性:obj.foo
- 判斷對(duì)象或原型上是否存在給定的key;key in obkj
- 使用for … in循環(huán)遍歷對(duì)象
首先對(duì)于基本的訪問屬性,我們可以使用get方法攔截。
const obj = new Proxy(data, {
get(target, key, receiver) { // get 接收第三個(gè)參數(shù),即操作的調(diào)用者,對(duì)應(yīng)obj.fn()就是obj了
track(target, key);
return Reflect.get(target, key, receiver); // 將原來直接返回target[key]的操作改為Reflect.get
}
}
然后,對(duì)于in操作符,我們使用has方法進(jìn)行攔截。
has(target, key) {
track(target, key);
return Reflect.has(target,key);
}
最后,對(duì)于for … in操作,我們使用ownKeys方法進(jìn)行攔截。這里使用和唯一標(biāo)識(shí)ITERATE_KEY和副作用函數(shù)綁定,因?yàn)閷?duì)于ownKeys操作來說,無論如何它都是對(duì)一個(gè)對(duì)象上所存在的所有屬性進(jìn)行遍歷,并不會(huì)產(chǎn)生實(shí)際的屬性讀取操作,因此我們需要用一個(gè)唯一的標(biāo)識(shí)來標(biāo)記ownKeys操作。
ownKeys(target, key) {
// 這里將副作用函數(shù)和唯一標(biāo)識(shí)ITERATE_KEY綁定了
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
相應(yīng)的,在進(jìn)行賦值操作的時(shí)候,也需要相應(yīng)的對(duì)ITERATE_KEY這個(gè)標(biāo)識(shí)進(jìn)行處理
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const iterateEffects = depsMap.get(ITERATE_KEY); // 讀取ITERATE_KEY
const effectToRun = new Set();
effects &&
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectToRun.add(fn);
}
});
// 將與 ITERATE_KEY 相關(guān)聯(lián)的副作用函數(shù)也添加到 effectsToRun
iterateEffects &&
iterateEffects.forEach((fn) => {
if (fn !== activeEffect) {
effectToRun.add(fn);
}
});
effectToRun.forEach((fn) => {
if (fn.options.scheduler) {
fn.options.scheduler(fn);
} else {
fn();
}
});
}
雖然以上的代碼解決了添加屬性的問題,但是隨之而來的是修改屬性的問題。對(duì)于for … in循環(huán)來說,無論原對(duì)象的屬性如何修改,對(duì)它來說只需要進(jìn)行一次遍歷就好了,因此我們需要區(qū)分添加和修改的操作。這里使用Object.prototype.hasOwnProperty檢查當(dāng)前操作的屬性是否已經(jīng)存在于目標(biāo)對(duì)象上,如果是,則說明當(dāng)前的操作類型是’SET‘,否則說明是’ADD‘。然后將type作為第三個(gè)參數(shù),傳入trigger函數(shù)中。
set(target, key, newVal, receiver) {
const type = Object.prototype.hasOwnProperty.call(target, key)
? "SET"
: "ADD";
Reflect.set(target, key, newVal, receiver);
trigger(target, key, type);
},
(2)代理delete操作符
代理delete操作符使用的是deleteProperty方法,因?yàn)閐elete操作符刪除屬性會(huì)導(dǎo)致屬性的數(shù)量變少,因此當(dāng)操作類型為DELETE時(shí)也要觸發(fā)一下for … in循環(huán)的操作。
deleteProperty(target, key) {
// 檢查刪除的key是否為自身屬性
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
if (res && hadKey) {
trigger(target, key, "DELETE");
}
return res;
},
// 當(dāng)type為ADD或DELETE的時(shí)候,才執(zhí)行ITERATE_KEY相關(guān)的操作
if (type === "ADD" || type === "DELETE") {
iterateEffects &&
iterateEffects.forEach((fn) => {
if (fn !== activeEffect) {
effectToRun.add(fn);
}
});
}
4.合理的觸發(fā)響應(yīng)
(1)完善響應(yīng)操作
觸發(fā)修改操作時(shí),若新值和舊值相等,則不需要觸發(fā)修改響應(yīng)操作。
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;
},
但是全等有一個(gè)特殊情況,就是NaN === NaN的值為false,因此我們需要對(duì)NaN進(jìn)行一個(gè)特殊判斷。
(2)封裝一個(gè)reactive函數(shù)
其實(shí)就是對(duì)new Proxy進(jìn)行了一個(gè)簡(jiǎn)單的封裝。
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
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);
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
// 比較新值和舊值
trigger(target, key, type);
}
return res;
},
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
ownKeys(target, key) {
// 這里將副作用函數(shù)和唯一標(biāo)識(shí)ITERATE_KEY綁定了
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
deleteProperty(target, key) {
// 檢查刪除的key是否為自身屬性
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
if (res && hadKey) {
trigger(target, key, "DELETE");
}
return res;
},
});
}
現(xiàn)在,我們使用reactive創(chuàng)建兩個(gè)響應(yīng)式對(duì)象,child和parent,然后將child原型設(shè)置為parent。然后為child.bar函數(shù)綁定副作用函數(shù)。當(dāng)修改child.bar的值的時(shí)候,可以看到,副作用函數(shù)實(shí)際執(zhí)行了兩次。這是因?yàn)椋琧hild的原型是parent,child本身并沒有bar這個(gè)屬性,所以根據(jù)原型鏈的規(guī)則,最終會(huì)在parent身上拿到bar這個(gè)屬性。因?yàn)樵谶M(jìn)行原型鏈查找的過程中,訪問到了parent上的屬性,因襲進(jìn)行了一次額外的綁定操作,所以最終副作用函數(shù)執(zhí)行了兩次。
const obj = {};
const proto = {
bar: 1,
};
const child = reactive(obj);
const parent = reactive(proto);
Object.setPrototypeOf(child, parent);
effect(() => {
console.log(child.bar);
});
child.bar = 2; // 輸出 1 2 2
這里我們比較一下child和parent的攔截函數(shù),可以發(fā)現(xiàn)receiver的值都是相同的,發(fā)生變化的是target的值,因此我們可以通過比較taregt的值來取消parent觸發(fā)的那一次響應(yīng)操作。
// child 的攔截函數(shù)
get(target, key, receiver) {
// target是原始對(duì)象obj
// receiver 是child
}
// parent 的攔截函數(shù)
get(target, key, receiver) {
// target是proto對(duì)象
// receiver 是child
}
這里我們通過添加一個(gè)raw操作來實(shí)現(xiàn),當(dāng)訪問raw屬性的時(shí)候,會(huì)返回該對(duì)象的target值。
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === "raw") {
// 添加一個(gè)新值 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);
if (target === receiver.raw) {
// 比較target值,如果receiver的target和當(dāng)前target相同,說明就不是原型鏈操作。
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
// 比較新值和舊值
trigger(target, key, type);
}
}
return res;
}
}
}
5.深響應(yīng)和淺響應(yīng)
實(shí)際上,前面我們實(shí)現(xiàn)的reactive還只是淺層響應(yīng),也就是說只有對(duì)象的第一層具有響應(yīng)式反應(yīng)。比如對(duì)于一個(gè)obj:{bar{val:1}}對(duì)象,當(dāng)對(duì)obj.bar.val進(jìn)行操作的時(shí)候,我們首先從obj中拿到bar,但是這時(shí)候的bar只是一個(gè)普通對(duì)象bar:{val:1},因此無法進(jìn)行響應(yīng)式操作。這里我們對(duì)Reflect.get獲取的值進(jìn)行一個(gè)判斷,如果拿到的值是一個(gè)對(duì)象,遞歸調(diào)用reactive函數(shù),最后拿到一個(gè)深層響應(yīng)的對(duì)象。
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === "raw") {
return target;
}
track(target, key);
const res = Reflect.get(target, key, receiver);
if(typeof res === 'object') {
return reactive(res);
}
return res;
}
}
}
但是我們并非所有時(shí)候都期望深層響應(yīng),因此我們調(diào)整一下reactive函數(shù)。
function createReactive(obj, isShallow = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === "raw") {
return target;
}
track(target, key);
const res = Reflect.get(target, key, receiver);
if (isShallow) return res; // 如果淺層響應(yīng),直接返回
if (typeof res === "object") {
return reactive(res);
}
return res;
},
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 (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
// 比較新值和舊值
trigger(target, key, type);
}
}
return res;
},
has(target, key) {
track(target, key);
return Reflect.has(target, key);
},
ownKeys(target, key) {
// 這里將副作用函數(shù)和唯一標(biāo)識(shí)ITERATE_KEY綁定了
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
},
deleteProperty(target, key) {
// 檢查刪除的key是否為自身屬性
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
if (res && hadKey) {
trigger(target, key, "DELETE");
}
return res;
},
});
}
function reactive(obj) {
return createReactive(obj, true);
}
function shallowReactive(obj) {
return createReactive(obj, false);
}
6.只讀和淺只讀
實(shí)現(xiàn)只讀其實(shí)只需要在createReactiv函數(shù)中添上第三個(gè)參數(shù)isReadOnly。
function createReactive(obj, isShallow = false, isReadOnly = false) {
return new Proxy(obj, {
set(target, key, newVal, receiver) {
if (isReadOnly) {
console.warn(`屬性${key}是只讀的`);
return true;
}
const oldVal = target[key]; // 獲取舊值
const type = Object.prototype.hasOwnProperty.call(target, key)
? "SET"
: "ADD";
const res = Reflect.set(target, key, newVal, receiver);
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
// 比較新值和舊值
trigger(target, key, type);
}
}
return res;
},
deleteProperty(target, key) {
if (isReadOnly) {
console.warn(`屬性${key}是只讀的`);
return true;
}
// 檢查刪除的key是否為自身屬性
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const res = Reflect.deleteProperty(target, key);
if (res && hadKey) {
trigger(target, key, "DELETE");
}
return res;
},
}
}
當(dāng)然,對(duì)于設(shè)置了只讀屬性的對(duì)象的屬性,很明顯就沒必要添加依賴了,所以對(duì)于get也要進(jìn)行相應(yīng)的修改.
function createReactive(obj, isShallow = false, isReadOnly = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === "raw") {
// 通過獲取raw屬性,拿到初始對(duì)象
return target;
}
if (!isReadOnly) {
// 只讀情況下不需要建立聯(lián)系
track(target, key);
}
const res = Reflect.get(target, key, receiver);
if (isShallow) return res; // 如果淺層響應(yīng),直接返回
if (typeof res === "object") {
// 如果獲取的值是對(duì)象,遞歸調(diào)用reactive函數(shù),得到深層響應(yīng)對(duì)象
return reactive(res);
}
return res;
},
}
}
但是,上述操作只能做到淺只讀,深只讀實(shí)現(xiàn)起來也很簡(jiǎn)單,判斷只讀標(biāo)記然后遞歸添加只讀屬性就行了.
function createReactive(obj, isShallow = false, isReadOnly = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === "raw") {
// 通過獲取raw屬性,拿到初始對(duì)象
return target;
}
if (!isReadOnly) {
// 只讀情況下不需要建立聯(lián)系
track(target, key);
}
const res = Reflect.get(target, key, receiver);
if (isShallow) return res; // 如果淺層響應(yīng),直接返回
if (typeof res === "object" && res !== null) {
// 如果獲取的值是對(duì)象,且只讀標(biāo)記的值為true,遞歸調(diào)用readonly函數(shù),得到深層只讀響應(yīng)對(duì)象.否則,遞歸調(diào)用reactive函數(shù),得到深層響應(yīng)對(duì)象
return isReadOnly ? readonly(res) : reactive(res);
}
return res;
},
然后和reactive函數(shù)一樣,封裝一下只讀readonly函數(shù).
function readonly(obj) {
return createReactive(obj, true, true);
}
function shallowReadonly(obj) {
return createReactive(obj, false, true);
}
7.代理數(shù)組
(1)讀取和修改操作
數(shù)組的讀取操作:
- 通過索引訪問元素,arr[0]
- 訪問數(shù)組長(zhǎng)度,arr.length
- for in循環(huán)訪問arr對(duì)象
- for of循環(huán)訪問arr對(duì)象
- 數(shù)組的原型方法,find,concat等
數(shù)組的修改操作:
- 通過索引修改數(shù)組,arr[0] = 1
- 修改數(shù)組長(zhǎng)度,arr.length = 1
- 數(shù)組的棧、隊(duì)列方法,arr.push
- 修改數(shù)組的原型方法,arr.slice,arr.sort等
對(duì)于通過索引訪問這一操作,它實(shí)際上和普通對(duì)象是一樣的,都可以通過get直接攔截。但是對(duì)于通過索引修改這一操作,就稍有不同了,因?yàn)槿绻?dāng)前設(shè)置的索引>數(shù)組長(zhǎng)度的話,相應(yīng)的也會(huì)對(duì)數(shù)組的長(zhǎng)度進(jìn)行修改,而且在修改數(shù)組長(zhǎng)度的過程中,還需要對(duì)數(shù)組長(zhǎng)度的修改做出響應(yīng)。同時(shí),直接修改數(shù)組的length屬性也會(huì)造成影響,如果小于當(dāng)前數(shù)組長(zhǎng)度,那么會(huì)對(duì)差值內(nèi)元素進(jìn)行清楚操作,否則則對(duì)之前的元素沒有影響。
首先我們對(duì)應(yīng)修改數(shù)組索引設(shè)置這一操作:
function createReactive(obj, isShallow = false, isReadOnly = false) {
return new Proxy(obj, {
set(target, key, newVal, receiver) {
if (isReadOnly) {
// 如果對(duì)象只讀,提示報(bào)錯(cuò)信息
console.warn(`屬性${key}是只讀的`);
return true;
}
const oldVal = target[key]; // 獲取舊值
// 判斷操作類型,如果是數(shù)組類型,則根據(jù)索引大小來判斷
const type = Array.isArray(target)
? Number(key) < target.length
? "SET"
: "ADD"
: Object.prototype.hasOwnProperty.call(target, key)
? "SET"
: "ADD"; // 獲取操作類型
const res = Reflect.set(target, key, newVal, receiver);
if (target === receiver.raw) {
if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
trigger(target, key, type, newVal); // 添加第四個(gè)參數(shù)
}
}
return res;
}
}
}
然后修改trigger函數(shù),判斷是否為數(shù)組和ADD操作,然后添加length屬性的相關(guān)操作
// trigger函數(shù)添加第四個(gè)參數(shù)newVal,即觸發(fā)響應(yīng)的值
function trigger(target, key, type) {
const depsMap = bucket.get(target); // 首先從對(duì)象桶中取出當(dāng)前對(duì)象的依賴表
if (!depsMap) return;
const effects = depsMap.get(key); // 從依賴表中拿到當(dāng)前鍵值的依賴集合
const iterateEffects = depsMap.get(ITERATE_KEY); // 嘗試獲取for in循環(huán)操作的依賴集合
const effectToRun = new Set(); // 創(chuàng)建依賴執(zhí)行隊(duì)列
if (type === "ADD" && Array.isArray(target)) {
// 如果操作類型是ADD且對(duì)象類型是數(shù)組,將length相關(guān)依賴添加到待執(zhí)行隊(duì)列中
const lengthEffects = depsMap.get("length");
lengthEffects &&
lengthEffects.forEach((fn) => {
if (fn !== activeEffect) {
effectToRun.add(effectFn);
}
});
}
if (Array.isArray(target) && key === "length") {
// 對(duì)于索引大于等于新length值的元素,需要將所有相關(guān)聯(lián)的函數(shù)取出添加到effectToRun中待執(zhí)行
if (key >= newVal) {
effects.forEach((fn) => {
if (fn !== activeEffect) {
effectToRun.add(fn);
}
});
}
}
(2)數(shù)組的遍歷
首先是for in循環(huán),會(huì)影響for in循環(huán)的操作主要是根據(jù)索引設(shè)置數(shù)組值和修改數(shù)組的length屬性,而這兩種操作,實(shí)際上都是對(duì)數(shù)組length值的操作,因此我們只需要在onwKeys方法里判斷,當(dāng)前操作的是否是數(shù)組,如果是數(shù)組的話,就使用length屬性作為key并建立聯(lián)系。
ownKeys(target, key) {
// 這里將副作用函數(shù)和唯一標(biāo)識(shí)ITERATE_KEY綁定了
track(target, Array.isArray(target) ? "length" : ITERATE_KEY); // 進(jìn)行依賴收集
return Reflect.ownKeys(target);
},
然后是for of循環(huán),它主要是通過和索引和length進(jìn)行操作,所以不需要進(jìn)行額外的操作,就可以實(shí)現(xiàn)依賴。但是在使用for of循環(huán)的時(shí)候,會(huì)對(duì)數(shù)組的Symbol.iterator屬性進(jìn)行讀取,該屬性是一個(gè)symbol值,為了避免發(fā)生意外錯(cuò)誤,以及性能上的考慮,需要對(duì)類型為了symbol的值進(jìn)行隔離。
function createReactive(obj, isShallow = false, isReadOnly = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === "raw") {
// 通過獲取raw屬性,拿到初始對(duì)象
return target;
}
if (!isReadOnly && typeof key !== "symbol") {
// 只讀情況和key值為symbol的情況下不需要建立聯(lián)系
track(target, key);
}
const res = Reflect.get(target, key, receiver);
if (isShallow) return res; // 如果淺層響應(yīng),直接返回
if (typeof res === "object" && res !== null) {
// 如果獲取的值是對(duì)象,遞歸調(diào)用reactive函數(shù),得到深層響應(yīng)對(duì)象
return isReadOnly ? readonly(res) : reactive(res);
}
return res;
},
}
}
(3)數(shù)組的查找方法
arr.includes方法在正常情況下是可以正常觸發(fā)綁定的,因?yàn)閍rr.include方法會(huì)在查找過程中訪問數(shù)組對(duì)象的length屬性和索引。但是在一些特殊的情況下,比如說數(shù)組元素是對(duì)象的情況下,在我們目前的響應(yīng)式系統(tǒng)下,就會(huì)出現(xiàn)一些特殊的情況。
const obj = {};
const arr = reactive([arr]);
console.log(arr.includes(arr)); // false
運(yùn)行上述代碼,得到的結(jié)果為false,這是因?yàn)樵谖覀冎按a設(shè)計(jì)中,如果讀取操作取到的值是一個(gè)可代理對(duì)象,那么我們會(huì)繼續(xù)對(duì)這個(gè)對(duì)象進(jìn)行代理。而進(jìn)行繼續(xù)代理后,得到的對(duì)象就是一個(gè)全新的對(duì)象了。
if (typeof res === "object" && res !== null) {
// 如果獲取的值是對(duì)象,遞歸調(diào)用reactive函數(shù),得到深層響應(yīng)對(duì)象
return isReadOnly ? readonly(res) : reactive(res);
}
對(duì)此,我們創(chuàng)建一個(gè)緩存Map,避免重復(fù)創(chuàng)建的問題。
const reactiveMap = new Map();
function reactive(obj) {
// 獲取當(dāng)前對(duì)象的緩存值
const existionProxy = reactiveMap.get(obj);
// 如果當(dāng)前對(duì)象存在緩存值,直接返回
if (existionProxy) return existionProxy;
// 否則創(chuàng)建新的響應(yīng)對(duì)象
const proxy = createReactive(obj, true);
// 緩存新對(duì)象
reactiveMap.set(obj, proxy);
return proxy;
}
但是這個(gè)時(shí)候我們又會(huì)碰到一個(gè)新問題,就是如果傳入原始對(duì)象,也就是obj的話,也會(huì)返回false,這是因?yàn)槲覀儠?huì)從arr中拿到的是響應(yīng)式對(duì)象,所以我們需要修改arr.includes的默認(rèn)行為。
const originMethod = Array.prototype.includes;
const arrayInstrumentations = {
includes: function (...args) {
// this是代理對(duì)象,先在代理對(duì)象中進(jìn)行查找
let res = originMethod.apply(this, args);
if (res === false) {
// 如果在代理對(duì)象上無法找到,再到原始對(duì)象上找
res = originMethod.apply(this.raw, args);
}
return res;
},
};
function createReactive(obj, isShallow = false, isReadOnly = false) {
return new Proxy(obj, {
get(target, key, receiver) {
if (key === "raw") {
// 通過獲取raw屬性,拿到初始對(duì)象
return target;
}
// 如果操作目標(biāo)是數(shù)組,而且key處于arrayInstrumentations之上,那么返回自定義的行為
if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
}
}
}
除了includes外,需要做類似處理的還有indexof和lastIndexOf
const arrayInstrumentations = {};
["includes", "indexof", "lastIndexof"].forEach((method) => {
const originMethod = Array.prototype[method];
arrayInstrumentations[method] = function (...args) {
// this 是代理對(duì)象,先在代理對(duì)象中查找,將結(jié)果存儲(chǔ)到 res 中
let res = originMethod.apply(this, args);
// res 為 false 說明沒找到,通過 this.raw 拿到原始數(shù)組,再去其中查找,并更新 res 值
if (res === false || res === -1) {
res = originMethod.apply(this.raw, args);
}
return res;
};
});
(4)隱式修改數(shù)組的方法
主要有push、pop、shift、unshift和splice,以push為例,push在添加元素的同時(shí),也會(huì)讀取length屬性,而這回導(dǎo)致兩個(gè)獨(dú)立的副作用函數(shù)相互影響。因此我們也需要重寫push操作,來避免這種情況的產(chǎn)生。這里我們添加一個(gè)是否進(jìn)行追蹤的標(biāo)記,在push方法執(zhí)行之前,將標(biāo)記置為false
let shouldTrack = true; // 是否進(jìn)行追蹤標(biāo)記
["push"].forEach((method) => {
const originMethod = Array.prototype[method];
arrayInstrumentations[method] = function (...args) {
// 在調(diào)用原始方法之前,禁止追蹤
shouldTrack = false;
// 默認(rèn)行為
let res = originMethod.apply(this, args);
// 在調(diào)用原始方法之后,恢復(fù)原來的行為,即允許追蹤
shouldTrack = true;
return res;
};
});
function track(target, key) {
if (!activeEffect || !shouldTrack) {
// 如果沒有當(dāng)前執(zhí)行的副作用函數(shù),不進(jìn)行處理
return;
}
}
? 最后,修改所以該類行為。
["push", "pop", "shift", "unshift", "splice"].forEach((method) => {
const originMethod = Array.prototype[method];
arrayInstrumentations[method] = function (...args) {
// 在調(diào)用原始方法之前,禁止追蹤
shouldTrack = false;
// 默認(rèn)行為
let res = originMethod.apply(this, args);
// 在調(diào)用原始方法之后,恢復(fù)原來的行為,即允許追蹤
shouldTrack = true;
return res;
};
});
到此這篇關(guān)于Vue reactive函數(shù)實(shí)現(xiàn)流程詳解的文章就介紹到這了,更多相關(guān)Vue reactive函數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue實(shí)現(xiàn)JSON字符串格式化編輯器組件功能
這篇文章主要介紹了Vue實(shí)現(xiàn)JSON字符串格式化編輯器組件,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01
安裝node.js以及搭建vue項(xiàng)目過程中遇到的問題詳解
為了讓一些不太清楚搭建前端項(xiàng)目的小白,更快上手,下面這篇文章主要給大家介紹了關(guān)于安裝node.js以及搭建vue項(xiàng)目過程中遇到問題的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06
vue.js添加一些觸摸事件以及安裝fastclick的實(shí)例
今天小編就為大家分享一篇vue.js添加一些觸摸事件以及安裝fastclick的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-08-08
vue項(xiàng)目用后端返回的文件流實(shí)現(xiàn)docx和pdf文件預(yù)覽
本文主要介紹了vue項(xiàng)目用后端返回的文件流實(shí)現(xiàn)docx和pdf文件預(yù)覽,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
element table列表根據(jù)數(shù)據(jù)設(shè)置背景色
在使用elementui中的el-table時(shí),需要將表的背景色和字體顏色設(shè)置為新顏色,本文就來介紹一下element table列表根據(jù)數(shù)據(jù)設(shè)置背景色,感興趣的可以了解一下2023-08-08

