狂肝半個月!1.3 萬字深度剖析 Vue3 響應(yīng)式(附腦圖)
寫在前面
本文的目標(biāo)是實現(xiàn)一個基本的vue3的響應(yīng)式,包含最基礎(chǔ)的情況的處理,本文是系列文章,如果你對vue3還不了解,那么請移步:
超詳細(xì)整理vue3基礎(chǔ)知識?? [1]
本文你將學(xué)到
- 一個基礎(chǔ)的響應(yīng)式實現(xiàn) ?
- Proxy ?
- Reflect ?
- 嵌套effect的實現(xiàn) ?
- computed ?
- watch ?
- 淺響應(yīng)與深響應(yīng) ?
- 淺只讀與深只讀 ?
- 處理數(shù)組長度 ?
- ref ?
- toRefs ?
響應(yīng)式.png一. 實現(xiàn)一個完善的響應(yīng)式
所謂的響應(yīng)式數(shù)據(jù)的概念,其實最主要的目的就是為數(shù)據(jù)綁定執(zhí)行函數(shù),當(dāng)數(shù)據(jù)發(fā)生變動的時候,再次觸發(fā)函數(shù)的執(zhí)行。
例如我們有一個對象data,我們想讓它變成一個響應(yīng)式數(shù)據(jù),當(dāng)data的數(shù)據(jù)發(fā)生變化時,自動執(zhí)行effect函數(shù),使nextVal變量的值也進(jìn)行變化:
//?定義一個對象
let?data?=?{
??name:?'pino',
??age:?18
}
let?nextVal
//?待綁定函數(shù)
function?effect()?{
??nextVal?=?data.age?+?1
}
data.age++
復(fù)制代碼
上面的例子中我們將data中的age的值進(jìn)行變化,但是effect函數(shù)并沒有執(zhí)行,因為現(xiàn)在effect函數(shù)與data這個對象不能說是沒啥聯(lián)系,簡直就是半毛錢的關(guān)系都沒有。
那么怎么才能使這兩個毫不相關(guān)的函數(shù)與對象之間產(chǎn)生關(guān)聯(lián)呢?
因為一個對象最好可以綁定多個函數(shù),所以有沒有可能我們?yōu)?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">data這個對象定義一個空間,每當(dāng)data的值進(jìn)行變化的時候就會執(zhí)行這個空間里的函數(shù)?
答案是有的。
1. Object.defineProperty()
js在原生提供了一個用于操作對象的比較底層的api:Object.defineProperty(),它賦予了我們對一個對象的讀取和攔截的操作。
Object.defineProperty()方法直接在一個對象上定義一個新屬性,或者修改一個已經(jīng)存在的屬性, 并返回這個對象。
??Object.defineProperty(obj,?prop,?descriptor)
復(fù)制代碼
參數(shù)
obj 需要定義屬性的對象。prop 需被定義或修改的屬性名。descriptor (描述符) 需被定義或修改的屬性的描述符。
其中descriptor接受一個對象,對象中可以定義以下的屬性描述符,使用屬性描述符對一個對象進(jìn)行攔截和控制:
-
value——當(dāng)試圖獲取屬性時所返回的值。 -
writable——該屬性是否可寫。 -
enumerable——該屬性在for in循環(huán)中是否會被枚舉。 -
configurable——該屬性是否可被刪除。 -
set()——該屬性的更新操作所調(diào)用的函數(shù)。 -
get()——獲取屬性值時所調(diào)用的函數(shù)。
另外,數(shù)據(jù)描述符(其中屬性為:enumerable , configurable , value , writable )與存取描述符(其中屬性為 enumerable , configurable , set() , get() )之間是有互斥關(guān)系的。在定義了 set() 和 get() 之后,描述符會認(rèn)為存取操作已被 定義了,其中再定義 value 和 writable 會引起錯誤。
?let?obj?=?{
???name:?"小花"
?}
?Object.defineProperty(obj,?'name',?{
???//?屬性讀取時進(jìn)行攔截
???get()?{?return?'小明';?},
???//?屬性設(shè)置時攔截
???set(newValue)?{?obj.name?=?newValue;?},
???enumerable:?true,
???configurable:?true
?});
復(fù)制代碼
上面的例子中就已經(jīng)完成對一個對象的最基本的攔截,這也是vue2.x中對對象監(jiān)聽的方式,但是由于Object.defineProperty()中存在一些問題,例如:
-
一次只能對一個屬性進(jìn)行監(jiān)聽,需要遍歷來對所有屬性監(jiān)聽
-
對于對象的新增屬性,需要手動監(jiān)聽
-
對于數(shù)組通過
push、unshift方法增加的元素,也無法監(jiān)聽
那么vue3版本中是如何對一個對象進(jìn)行攔截的呢?答案是es6中的Proxy。
由于本文主要是vue3版本的響應(yīng)式的實現(xiàn),如果想要深入了解Object.defineProperty(),請移步:
MDN Object.defineProperty [2]
2. Proxy
proxy是es6版本出現(xiàn)的一種對對象的操作方式,Proxy?可以理解成,在目標(biāo)對象之前架設(shè)一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進(jìn)行過濾和改寫。Proxy 這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
通過proxy我們可以實現(xiàn)對一個對象的讀取,設(shè)置等等操作進(jìn)行攔截,而且直接對對象進(jìn)行整體攔截,內(nèi)部提供了多達(dá)13種攔截方式。
-
get(target, propKey, receiver) :攔截對象屬性的讀取,比如
proxy.foo和proxy['foo']。 -
set(target, propKey, value, receiver) :攔截對象屬性的設(shè)置,比如
proxy.foo = v或proxy['foo'] = v,返回一個布爾值。 -
has(target, propKey) :攔截
propKey in proxy的操作,返回一個布爾值。 -
deleteProperty(target, propKey) :攔截
delete proxy[propKey]的操作,返回一個布爾值。 -
ownKeys(target) :攔截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循環(huán),返回一個數(shù)組。該方法返回目標(biāo)對象所有自身的屬性的屬性名,而Object.keys()的返回結(jié)果僅包括目標(biāo)對象自身的可遍歷屬性。 -
getOwnPropertyDescriptor(target, propKey) :攔截
Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。 -
defineProperty(target, propKey, propDesc) :攔截
Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。 -
preventExtensions(target) :攔截
Object.preventExtensions(proxy),返回一個布爾值。 -
getPrototypeOf(target) :攔截
Object.getPrototypeOf(proxy),返回一個對象。 -
isExtensible(target) :攔截
Object.isExtensible(proxy),返回一個布爾值。 -
setPrototypeOf(target, proto) :攔截
Object.setPrototypeOf(proxy, proto),返回一個布爾值。如果目標(biāo)對象是函數(shù),那么還有兩種額外操作可以攔截。 -
apply(target, object, args) :攔截 Proxy (代理)?實例作為函數(shù)調(diào)用的操作,比如
proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 -
construct(target, args) :攔截 Proxy (代理)?實例作為構(gòu)造函數(shù)調(diào)用的操作,比如
new proxy(...args)。
如果想要詳細(xì)了解proxy,請移步:
es6.ruanyifeng.com/#docs/proxy… [3]
?let?obj?=?{
???name:?"小花"
?}
?//?只使用get和set進(jìn)行演示
?let?obj2?=?new?Proxy(obj,?{
???//?讀取攔截
???get:?function?(target,?propKey)?{
?????return?target[propKey]
???},
???//?設(shè)置攔截
???set:?function?(target,?propKey,?value)?{
?????//?此處的value為用戶設(shè)置的新值
?????target[propKey]?=?value
???}
?});
復(fù)制代碼
3. 一個最簡單的響應(yīng)式
有了proxy,我們就可以根據(jù)之前的思路實現(xiàn)一個基本的響應(yīng)式功能了,我們的思路是這樣的:在對象被讀取時把函數(shù)收集到一個“倉庫”,在對象的值被設(shè)置時觸發(fā)倉庫中的函數(shù)。
由此我們可以寫出一個最基本的響應(yīng)式功能:
//?定義一個“倉庫”,用于存儲觸發(fā)函數(shù)
let?store?=?new?Set()
//?使用proxy進(jìn)行代理
let?data_proxy?=?new?Proxy(data,?{
??//?攔截讀取操作
??get(target,?key)?{
????//?收集依賴函數(shù)
????store.add(effect)
????return?target[key]
??},
??//?攔截設(shè)置操作
??set(target,?key,?newVal)?{
????target[key]?=?newVal
????//?取出所有的依賴函數(shù),執(zhí)行
????store.forEach(fn?=>?fn())
??}
})
復(fù)制代碼
我們創(chuàng)建了一個用于保存依賴函數(shù)的“倉庫”,它是Set類型,然后使用proxy對對象data進(jìn)行代理,設(shè)置了set和get攔截函數(shù),用于攔截讀取和設(shè)置操作,當(dāng)讀取屬性時,將依賴函數(shù)effect存儲到“倉庫”中,當(dāng)設(shè)置屬性值時,將依賴函數(shù)從“倉庫”中取出并重新執(zhí)行。
還有一個小問題,怎么觸發(fā)對象的讀取操作呢?我們可以直接調(diào)用一次effect函數(shù),如果在effect函數(shù)中存在需要收集的屬性,那么執(zhí)行一次effect函數(shù)也是比較符合常理的。
// 定義一個對象
let data = {
name: 'pino',
age: 18
}
let nextVal
// 待綁定函數(shù)
function effect() {
// 依賴函數(shù)在這里被收集
// 當(dāng)調(diào)用data.age時,effect函數(shù)被收集到“倉庫”中
nextVal = data.age + 1
console.log(nextVal)
}
// 執(zhí)行依賴函數(shù)
effect() // 19
setTimeout(()=>{
// 使用proxy進(jìn)行代理后,使用代理后的對象名
// 觸發(fā)設(shè)置操作,此時會取出effect函數(shù)進(jìn)行執(zhí)行
data_proxy.age++ // 2秒后輸出 20
}, 2000)
復(fù)制代碼
一開始會執(zhí)行一次effect,然后函數(shù)兩秒鐘后會執(zhí)行代理對象設(shè)置操作,再次執(zhí)行effect函數(shù),輸出20。
Jul-24-2022 17-31-39.gif此時整個響應(yīng)式流程的功能是這樣的:
階段一,在屬性被讀取時,為對象屬性收集依賴函數(shù):
image.png階段二,當(dāng)屬性發(fā)生改變時,再次觸發(fā)依賴函數(shù)
image.png這樣就實現(xiàn)了一個最基本的響應(yīng)式的功能。
4. 完善
問題一
其實上面實現(xiàn)的功能還有很大的缺陷,首先最明顯的問題是,我們把effect函數(shù)給固定了,如果用戶使用的依賴函數(shù)不叫effect怎么辦,顯然我們的功能就不能正常運行了。
所以先來進(jìn)行第一步的優(yōu)化:抽離出一個公共方法,依賴函數(shù)由用戶來傳遞參數(shù)。
我們使用effect函數(shù)來接受用戶傳遞的依賴函數(shù):
//?effect接受一個函數(shù),把這個匿名函數(shù)當(dāng)作依賴函數(shù)
function?effect(fn)?{
??//?執(zhí)行依賴函數(shù)
??fn()
}
//?使用
effect(()=>{
??nextVal?=?data.age?+?1
??console.log(nextVal)
})
復(fù)制代碼
但是effect函數(shù)內(nèi)部只是執(zhí)行了,在get函數(shù)中怎么能知道用戶傳遞的依賴函數(shù)是什么呢,這兩個操作并不在一個函數(shù)內(nèi)啊?其實可以使用一個全局變量activeEffect來保存當(dāng)前正在處理的依賴函數(shù)。
修改后的effect函數(shù)是這樣的:
let?activeEffect?//?新增
function?effect(fn)?{
??//?保存到全局變量activeEffect
??activeEffect?=?fn?//?新增
??//?執(zhí)行依賴函數(shù)
??fn()
}
//?而在get內(nèi)部只需要?收集activeEffect即可
get(target,?key)?{
??store.add(activeEffect)
??return?target[key]
},
復(fù)制代碼
調(diào)用effect函數(shù)傳遞一個匿名函數(shù)作為依賴函數(shù),當(dāng)執(zhí)行時,首先會把匿名函數(shù)賦值給全局變量activeEffect,然后觸發(fā)屬性的讀取操作,進(jìn)而觸發(fā)get攔截,將全局變量activeEffect進(jìn)行收集。
問題二
從上面我們定義的對象可以看到,我們的對象data中有兩個屬性,上面的例子中我們只給age建立了響應(yīng)式連接,那么如果我現(xiàn)在也想給name建立響應(yīng)式連接怎么辦呢?那好說,那我們直接向“倉庫”中繼續(xù)添加依賴函數(shù)不就行了嗎。
其實這會帶來很嚴(yán)重的問題,由于 “倉庫”并沒有與被操作的目標(biāo)屬性之間建立聯(lián)系,而上面我們的實現(xiàn)只是將整個“倉庫”遍歷了一遍,所以無論哪個屬性被觸發(fā),都會將“倉庫”中所有的依賴函數(shù)都取出來執(zhí)行一遍,因為整個執(zhí)行程序中可能有很多對象及屬性都設(shè)置了響應(yīng)式聯(lián)系,這將會帶來很大的性能浪費。所謂牽一發(fā)而動全身,這種結(jié)果顯然不是我們想要的。
let?data?=?{
??name:?'pino',
??age:?18
}
復(fù)制代碼
image.png所以我們要重新設(shè)計一下“倉庫”的數(shù)據(jù)結(jié)構(gòu),目的就是為了可以在屬性這個粒度下和“倉庫”建立明確的聯(lián)系。
就拿我們上面進(jìn)行操作的對象來說,存在著兩層的結(jié)構(gòu),有兩個角色,對象data以及屬性name``age
let?data?=?{
?name:?'pino',
?age:?18
}
復(fù)制代碼
他們的關(guān)系是這樣的:
data
???????->?name
???????????????->?effectFn
//?如果兩個屬性讀取了同一個依賴函數(shù)
data
???????->?name
???????????????->?effectFn
???????->?age
???????????????->?effectFn
//?如果兩個屬性讀取了不同的依賴函數(shù)
data
???????->?name
???????????????->?effectFn
???????->?age
???????????????->?effectFn1
//?如果是兩個不同的對象
data
???????->?name
???????????????->?effectFn
???????->?age
???????????????->?effectFn1
data2
???????->?addr
???????????????->?effectFn
復(fù)制代碼
接下來我們實現(xiàn)一下代碼,為了方便調(diào)用,將設(shè)置響應(yīng)式數(shù)據(jù)的操作封裝為一個函數(shù)reactive:
let?newObj?=?new?Proxy(obj,?{
??//?讀取攔截
??get:?function?(target,?propKey)?{
??},
??//?設(shè)置攔截
??set:?function?(target,?propKey,?value)?{
??}
});
//?封裝為
function?reactive(obj)?{
??return?new?Proxy(obj,?{
????//?讀取攔截
????get:?function?(target,?propKey)?{
????},
????//?設(shè)置攔截
????set:?function?(target,?propKey,?value)?{
????}
??});
}
復(fù)制代碼
function?reactive(obj)?{
??return?new?Proxy(obj,?{
????get(target,?key)?{
??????//?收集依賴
??????track(target,?key)
??????return?target[key]
????},
????set(target,?key,?newVal)?{
??????target[key]?=?newVal
??????//?觸發(fā)依賴
??????trigger(target,?key)
????}
??})
}
function?track(target,?key)?{
??//?如果沒有依賴函數(shù),則不需要進(jìn)行收集。直接return
??if?(!activeEffect)?return
??//?獲取target,也就是對象名,對應(yīng)上面例子中的data
??let?depsMap?=?store.get(target)
??if?(!depsMap)?{
????store.set(target,?(depsMap?=?new?Map()))
??}
??//?獲取對象中的key值,對應(yīng)上面例子中的name或age
??let?deps?=?depsMap.get(key)
??if?(!deps)?{
????depsMap.set(key,?(deps?=?new?Set()))
??}
??//?收集依賴函數(shù)
??deps.add(activeEffect)
}
function?trigger(target,?key)?{
??//?取出對象對應(yīng)的Map
??let?depsMap?=?store.get(target)
??if(!depsMap)?return
??//?取出key所對應(yīng)的Set
??let?deps?=?depsMap.get(key)
??//?執(zhí)行依賴函數(shù)
??deps?&&?deps.forEach(fn?=>?fn());
}
復(fù)制代碼
我們將讀取操作封裝為了函數(shù)track,觸發(fā)依賴函數(shù)的動作封裝為了trigger方便調(diào)用,現(xiàn)在的整個“倉庫”結(jié)構(gòu)是這樣的:
image.pngWeakMap
可能有人會問了,為什么設(shè)置“倉庫”要使用WeakMap呢,我使用一個普通對象來創(chuàng)建不行嗎?-
WeakMap 結(jié)構(gòu)與 Map 結(jié)構(gòu)類似,也是用于生成鍵值對的集合。
WeakMap 與 Map 的區(qū)別有兩點。
首先, WeakMap 只接受對象作為鍵名( null 除外),不接受其他類型的值作為鍵名。
const?map?=?new?WeakMap();
map.set(1,?2)
//?TypeError:?1?is?not?an?object!
map.set(Symbol(),?2)
//?TypeError:?Invalid?value?used?as?weak?map?key
map.set(null,?2)
//?TypeError:?Invalid?value?used?as?weak?map?key
復(fù)制代碼
上面代碼中,如果將數(shù)值 1 和 Symbol 值作為 WeakMap 的鍵名,都會報錯。
其次, WeakMap 的鍵名所指向的對象,不計入垃圾回收機制。
WeakMap 的設(shè)計目的在于,有時我們想在某個對象上面存放一些數(shù)據(jù),但是這會形成對于這個對象的引用。請看下面的例子。
const?e1?=?document.getElementById('foo');
const?e2?=?document.getElementById('bar');
const?arr?=?[
????[e1,?'foo?元素'],
????[e2,?'bar?元素'],
];
復(fù)制代碼
上面代碼中, e1 和 e2 是兩個對象,我們通過 arr 數(shù)組對這兩個對象添加一些文字說明。這就形成了 arr 對 e1 和 e2 的引用。
一旦不再需要這兩個對象,我們就必須手動刪除這個引用,否則垃圾回收機制就不會釋放 e1 和 e2 占用的內(nèi)存。
//?不需要?e1?和?e2?的時候
//?必須手動刪除引用
arr?[0]?=?null;
arr?[1]?=?null;
復(fù)制代碼
上面這樣的寫法顯然很不方便。一旦忘了寫,就會造成內(nèi)存泄露。
它的鍵名所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內(nèi)。因此,只要所引用的對象的其他引用都被清除,垃圾回收機制就會釋放該對象所占用的內(nèi)存。也就是說,一旦不再需要,WeakMap 里面的鍵名對象和所對應(yīng)的鍵值對會自動消失,不用手動刪除引用。
如果我們上文中target對象沒有任何引用了,那么說明用戶已經(jīng)不需要用到它了,這時垃圾回收器會自動執(zhí)行回收,而如果使用Map來進(jìn)行收集,那么即使其他地方的代碼已經(jīng)對target沒有任何引用,這個target也不會被回收。
Reflect
在vue3中的實現(xiàn)方式和我們的基本實現(xiàn)還有一點不同就是在vue3中是使用Reflect來操作數(shù)據(jù)的,例如:
function?reactive(obj)?{
?return?new?Proxy(obj,?{
???get(target,?key,?receiver)?{
?????track(target,?key)
?????//?使用Reflect.get操作讀取數(shù)據(jù)
?????return?Reflect.get(target,?key,?receiver)
???},
???set(target,?key,?value,?receiver)?{
?????trigger(target,?key)
?????//?使用Reflect.set來操作觸發(fā)數(shù)據(jù)
?????Reflect.set(target,?key,?value,?receiver)
???}
?})
}
復(fù)制代碼
那么為什么要使用Reflect來操作數(shù)據(jù)呢,像之前一樣直接操作原對象不行嗎,我們先來看一下一種特殊的情況:
const?obj?=?{
??foo:?1,
??get?bar()?{
????return?this.foo
??}
}
復(fù)制代碼
在effect依賴函數(shù)中通過代理對象p訪問bar屬性:
effect(()=>{
??console.log(p.bar)?//?1
})
復(fù)制代碼
可以分析一下這個過程發(fā)生了什么,當(dāng)effect函數(shù)被調(diào)用時,會讀取p.bar屬性,他發(fā)現(xiàn)p.bar屬性是一個訪問器屬性,因此會執(zhí)行getter函數(shù),由于在getter函數(shù)中通過this.foo讀取了foo屬性的值,因此我們會認(rèn)為副作用函數(shù)與屬性foo之間也會建立聯(lián)系,當(dāng)修改p.foo的值的時候因該也能夠觸發(fā)響應(yīng),使依賴函數(shù)重新執(zhí)行才對,然而當(dāng)修改p.foo的時候,并沒有觸發(fā)依賴函數(shù):
p.foo++
復(fù)制代碼
實際上問題就出在bar屬性中的訪問器函數(shù)getter上:
get?bar()?{
??//?這個this究竟指向誰?
??return?this.foo
}
復(fù)制代碼
當(dāng)通過代理對象p訪問p.bar,這回觸發(fā)代理對象的get攔截函數(shù)執(zhí)行:
const?p?=?new?Proxt(obj,?{
??get(target,?key)?{
????track(target,?key)
????return?target[key]
??}
})
復(fù)制代碼
可以看到在get的攔截函數(shù)中,通過target[key]返回屬性值,其中target是原始對象obj,而key就是字符串'bar',所以target[key]就相當(dāng)于obj.bar。因此當(dāng)我們使用p.bar訪問bar屬性時,他的getter函數(shù)內(nèi)的this其實指向原始對象obj,這說明我們最終訪問的是obj.foo。所以在依賴函數(shù)內(nèi)部通過原始對象訪問他的某個屬性是不會建立響應(yīng)聯(lián)系的:
effect(()=>{
??//?obj是原始數(shù)據(jù),不是代理對象,不會建立響應(yīng)聯(lián)系
??obj.foo
})
復(fù)制代碼
那么怎么解決這個問題呢,這時候就需要用到 Reflect出場了。
先來看一下Reflect是啥:
Reflect函數(shù)的功能就是提供了訪問一個對象屬性的默認(rèn)行為,例如下面兩個操作是等價的:
const?obj?=?{?foo:?1?}
//?直接讀取
console.log(obj.foo)?//1
//?使用Reflect.get讀取
console.log(Reflect.get(obj,?'foo'))?//?1
復(fù)制代碼
實際上Reflect.get函數(shù)還能接受第三個函數(shù),即制定接受者receiver,可以把它理解為函數(shù)調(diào)用過程中的this:
const?obj?=?{?foo:?1?}
console.log(Reflect.get(obj,?'foo',?{?foo:?2?}))?//?輸出的是?2?而不是?1
復(fù)制代碼
在這段代碼中,指定了第三個參數(shù)receiver為一個對象{ foo: 2 },這是讀取到的值時receiver對象的foo屬性。
而我們上文中的問題的解決方法就是在操作對象數(shù)據(jù)的時候通過Reflect的方法來傳遞第三個參數(shù)receiver,它代表誰在讀取屬性:
const?p?=?new?Proxt(obj,?{
??//?讀取屬性接收receiver
??get(target,?key,?receiver)?{
????track(target,?key)
????//?使用Reflect.get返回讀取到的屬性值
????return?Reflect.get(target,?key,?receiver)
??}
})
復(fù)制代碼
當(dāng)使用代理對象p訪問bar屬性時,那么receiver就是p,可以把它理解為函數(shù)調(diào)用中的this。
所以我們改造一下reactive函數(shù)的實現(xiàn):
function?reactive(obj)?{
?return?new?Proxy(obj,?{
???get(target,?key,?receiver)?{
?????track(target,?key)
?????return?Reflect.get(target,?key,?receiver)
???},
???set(target,?key,?value,?receiver)?{
?????trigger(target,?key)
?????Reflect.set(target,?key,?value,?receiver)
???}
?})
}
復(fù)制代碼
擴展
Proxy -> get()
get 方法用于攔截某個屬性的讀取操作,可以接受三個參數(shù),依次為目標(biāo)對象、屬性名和?proxy?(代理)?實例本身(嚴(yán)格地說,是操作行為所針對的對象),其中最后一個參數(shù)可選。
Reflect.get(target, name, receiver)
Reflect.get 方法查找并返回 target 對象的 name 屬性,如果沒有該屬性,則返回 undefined 。
var?myObject?=?{
foo:?1,
bar:?2,
get?baz()?{
??return?this.foo?+?this.bar;
},
}
Reflect.get(myObject,?'foo')?//?1
Reflect.get(myObject,?'bar')?//?2
Reflect.get(myObject,?'baz')?//?3
復(fù)制代碼
如果 name 屬性部署了讀取函數(shù)(?getter?),則讀取函數(shù)的 this 綁定 receiver 。
var?myObject?=?{
foo:?1,
bar:?2,
get?baz()?{
??return?this.foo?+?this.bar;
},
};
var?myReceiverObject?=?{
foo:?4,
bar:?4,
};
Reflect.get(myObject,?'baz',?myReceiverObject)?//?8
復(fù)制代碼
如果第一個參數(shù)不是對象, Reflect.get 方法會報錯。
Reflect.get(1,?'foo')?//?報錯
Reflect.get(false,?'foo')?//?報錯
復(fù)制代碼
Reflect.set(target, name, value, receiver)
Reflect.set 方法設(shè)置 target 對象的 name 屬性等于 value 。
var?myObject?=?{
foo:?1,
set?bar(value)?{
??return?this.foo?=?value;
},
}
myObject.foo?//?1
Reflect.set(myObject,?'foo',?2);
myObject.foo?//?2
Reflect.set(myObject,?'bar',?3)
myObject.foo?//?3
復(fù)制代碼
如果 name 屬性設(shè)置了賦值函數(shù),則賦值函數(shù)的 this 綁定 receiver 。
var?myObject?=?{
foo:?4,
set?bar(value)?{
??return?this.foo?=?value;
},
};
var?myReceiverObject?=?{
foo:?0,
};
Reflect.set(myObject,?'bar',?1,?myReceiverObject);
myObject.foo?//?4
myReceiverObject.foo?//?1
復(fù)制代碼
注意,如果? Proxy 對象和? Reflect 對象聯(lián)合使用,前者攔截賦值操作,后者完成賦值的默認(rèn)行為,而且傳入了 receiver ,那么 Reflect.set 會觸發(fā) Proxy.defineProperty 攔截。
let?p?=?{
a:?'a'
};
let?handler?=?{
set(target,?key,?value,?receiver)?{
??console.log('set');
??Reflect.set(target,?key,?value,?receiver)
},
defineProperty(target,?key,?attribute)?{
??console.log('defineProperty');
??Reflect.defineProperty(target,?key,?attribute);
}
};
let?obj?=?new?Proxy(p,?handler);
obj.a?=?'A';
//?set
//?defineProperty
復(fù)制代碼
上面代碼中, Proxy.set 攔截里面使用了 Reflect.set ,而且傳入了 receiver ,導(dǎo)致觸發(fā) Proxy.defineProperty 攔截。這是因為 Proxy.set 的 receiver 參數(shù)總是指向當(dāng)前的? Proxy 實例(即上例的 obj ),而 Reflect.set 一旦傳入 receiver ,就會將屬性賦值到 receiver 上面(即 obj ),導(dǎo)致觸發(fā) defineProperty 攔截。如果 Reflect.set 沒有傳入 receiver ,那么就不會觸發(fā) defineProperty 攔截。
let?p?=?{
a:?'a'
};
let?handler?=?{
set(target,?key,?value,?receiver)?{
??console.log('set');
??Reflect.set(target,?key,?value)
},
defineProperty(target,?key,?attribute)?{
??console.log('defineProperty');
??Reflect.defineProperty(target,?key,?attribute);
}
};
let?obj?=?new?Proxy(p,?handler);
obj.a?=?'A';
//?set
復(fù)制代碼
如果第一個參數(shù)不是對象, Reflect.set 會報錯。
Reflect.set(1,?'foo',?{})?//?報錯
Reflect.set(false,?'foo',?{})?//?報錯
復(fù)制代碼
到這里,一個非常基本的響應(yīng)式的功能就完成了,整體代碼如下:
//?定義倉庫
let?store?=?new?WeakMap()
//?定義當(dāng)前處理的依賴函數(shù)
let?activeEffect
function?effect(fn)?{
??//?將操作包裝為一個函數(shù)
??const?effectFn?=?()=>?{
????activeEffect?=?effectFn
????fn()
??}
??effectFn()
}
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)?{
??????//?觸發(fā)依賴
??????trigger(target,?key)
??????Reflect.set(target,?key,?newVal,?receiver)
????}
??})
}
function?track(target,?key)?{
??//?如果沒有依賴函數(shù),則不需要進(jìn)行收集。直接return
??if?(!activeEffect)?return
??//?獲取target,也就是對象名
??let?depsMap?=?store.get(target)
??if?(!depsMap)?{
????store.set(target,?(depsMap?=?new?Map()))
??}
??//?獲取對象中的key值
??let?deps?=?depsMap.get(key)
??if?(!deps)?{
????depsMap.set(key,?(deps?=?new?Set()))
??}
??//?收集依賴函數(shù)
??deps.add(activeEffect)
}
function?trigger(target,?key)?{
??//?取出對象對應(yīng)的Map
??let?depsMap?=?store.get(target)
??if?(!depsMap)?return
??//?取出key所對應(yīng)的Set
??const?effects?=?depsMap.get(key)
??//?執(zhí)行依賴函數(shù)
??//?為避免污染,創(chuàng)建一個新的Set來進(jìn)行執(zhí)行依賴函數(shù)
??let?effectsToRun?=?new?Set()
??effects?&&?effects.forEach(effectFn?=>?{
??????effectsToRun.add(effectFn)
??})
??effectsToRun.forEach(effect?=>?effect())
}
復(fù)制代碼
二. 嵌套effect
在日常的工作中,effect函數(shù)并不是單獨存在的,比如在vue的渲染函數(shù)中,各個組件之間互相嵌套,那么他們在組件中所使用的effect是必然會發(fā)生嵌套的:
effect(function?effectFn1()?{
??effect(function?effectFn1()?{
????//?...
??})
})
復(fù)制代碼
當(dāng)組件中發(fā)生嵌套時,此時的渲染函數(shù):
effect(()=>{
??Father.render()
??//嵌套子組件
??effect(()=>{
????Son.render()
??})
})
復(fù)制代碼
但是此時我們實現(xiàn)的effect并沒有這個能力,執(zhí)行下面這段代碼,并不會出現(xiàn)意料之中的行為:
const?data?=?{?foo:?'pino',?bar:?'在干啥'?}
//?創(chuàng)建代理對象
const?obj?=?reactive(data)
let?p1,?p2;
//?設(shè)置obj.foo的依賴函數(shù)
effect(function?effect1(){
??console.log('effect1執(zhí)行');
??//?嵌套,obj.bar的依賴函數(shù)
??effect(function?effect2(){
????p2?=?obj.bar
????console.log('effect2執(zhí)行')
??})
??p1?=?obj.foo
})
復(fù)制代碼
在這段代碼中,定義了代理對象obj,里面有兩個屬性foo和bar,然后定義了收集foo的依賴函數(shù),在依賴函數(shù)的內(nèi)部又定義了bar的依賴函數(shù)。在理想狀態(tài)下,我們希望依賴函數(shù)與屬性之間的關(guān)系如下:
obj
????????->?foo
????????????????->?effect1
????????->?bar
????????????????->?effect2
復(fù)制代碼
當(dāng)修改obj.foo的值的時候,會觸發(fā)effect1函數(shù)執(zhí)行,由于effect2函數(shù)在effect函數(shù)內(nèi)部,所以effect2函數(shù)也會執(zhí)行,而當(dāng)修改obj.bar時,只會觸發(fā)effect2函數(shù)。接下來修改一下obj.foo:
const?data?=?{?foo:?'pino',?bar:?'在干啥'?}
//?創(chuàng)建代理對象
const?obj?=?reactive(data)
let?p1,?p2;
//?設(shè)置obj.foo的依賴函數(shù)
effect(function?effect1(){
??console.log('effect1執(zhí)行');
??//?嵌套,obj.bar的依賴函數(shù)
??effect(function?effect2(){
????p2?=?obj.bar
????console.log('effect2執(zhí)行')
??})
??p1?=?obj.foo
})
//?修改obj.foo的值
obj.foo?=?'前來買瓜'
復(fù)制代碼
看一下執(zhí)行結(jié)果:
image_1659170045716_0.png可以看到effect2函數(shù)竟然執(zhí)行了兩次?按照之前的分析,當(dāng)obj.foo被修改后,應(yīng)當(dāng)觸發(fā)effect1這個依賴函數(shù),但是為什么會effect2會被再次執(zhí)行呢?來看一下我們effect函數(shù)的實現(xiàn):
function?effect(fn)?{
??//?將依賴函數(shù)進(jìn)行包裝
??const?effectFn?=?()=>?{
????activeEffect?=?effectFn
????fn()
??}
??effectFn()
}
復(fù)制代碼
其實在這里就已經(jīng)很容易看出問題了,在接受用戶傳遞過來的值時,我們直接將activeEffect這個全局變量進(jìn)行了覆蓋!所以在內(nèi)部執(zhí)行完后,activeEffect這個變量就已經(jīng)是effect2函數(shù)了,而且永遠(yuǎn)不會再次變?yōu)?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">effect1,此時再進(jìn)行收集依賴函數(shù)時,永遠(yuǎn)收集的都是effect2函數(shù)。
那么如何解決這種問題呢,這種情況可以借鑒棧結(jié)構(gòu)來進(jìn)行處理,棧結(jié)構(gòu)是一種后進(jìn)先出的結(jié)構(gòu),在依賴函數(shù)執(zhí)行時,將當(dāng)前的依賴函數(shù)壓入棧中,等待依賴函數(shù)執(zhí)行完畢后將其從棧中彈出,始終activeEffect指向棧頂?shù)囊蕾嚭瘮?shù)。
//?增加effect調(diào)用棧
const?effectStack?=?[]?//?新增
function?effect(fn)?{
??let?effectFn?=?function?()?{
????activeEffect?=?effectFn
????//?入棧
????effectStack.push(effectFn)?//?新增
????//?執(zhí)行函數(shù)的時候進(jìn)行g(shù)et收集
????fn()
????//?收集完畢后彈出
????effectStack.pop()?//?新增
????//?始終指向棧頂
????activeEffect?=?effectStack[effectStack.length?-?1]?//?新增
??}
??effectFn()
}
復(fù)制代碼
未命名.drawio_1659171750374_0.png此時兩個屬性所對應(yīng)的依賴函數(shù)便不會發(fā)生錯亂了。
三. 避免無限循環(huán)
如果現(xiàn)在將effect函數(shù)中傳遞的依賴函數(shù)改一下:
//?定義一個對象
let?data?=?{
??name:?'pino',
??age:?18
}
//?將data更改為響應(yīng)式對象
let?obj?=?reactive(data)
effect(()?=>?{
??obj.age++
})
復(fù)制代碼
在這段代碼中,我們將代理對象obj的age屬性執(zhí)行自增操作,但是執(zhí)行這段代碼,卻發(fā)現(xiàn)竟然棧溢出了?這是怎么回事呢?
image_1659163246902_0.png其實在effect中處理依賴函數(shù)時,obj.age++的操作其實可以看做是這樣的:
effect(()=>{
??//?等式右邊的操作是先執(zhí)行了一次讀取操作
??obj.age?=?obj.age?+?1
})
復(fù)制代碼
這段代碼的執(zhí)行流程是這樣的:首先讀取obj.foo的值,這會觸發(fā)track函數(shù)進(jìn)行收集操作,也就是將當(dāng)前的依賴函數(shù)收集到“倉庫”中,接著將其加1后再賦值給obj.foo,此時會觸發(fā)trigger操作,即把“倉庫”中的依賴函數(shù)取出并執(zhí)行。但是此時該依賴函數(shù)正在執(zhí)行中,還沒有執(zhí)行完就要再次開始下一次的執(zhí)行。就會導(dǎo)致無限的遞歸調(diào)用自己。
解決這個問題,其實只需要在觸發(fā)函數(shù)執(zhí)行時,判斷當(dāng)前取出的依賴函數(shù)是否等于activeEffect,就可以避免重復(fù)執(zhí)行同一個依賴函數(shù)。
function?trigger(target,?key)?{
??//?取出對象對應(yīng)的Map
??let?depsMap?=?store.get(target)
??if?(!depsMap)?return
??//?取出key所對應(yīng)的Set
??const?effects?=?depsMap.get(key)
??//?//?執(zhí)行依賴函數(shù)
??//?因為刪除又添加都在同一個deps中,所以會產(chǎn)生無限執(zhí)行
??let?effectsToRun?=?new?Set()
??effects?&&?effects.forEach(effectFn?=>?{
????//?如果trigger出發(fā)執(zhí)行的副作用函數(shù)與當(dāng)前正在執(zhí)行的副作用函數(shù)相同,則不觸發(fā)執(zhí)行
????if?(effectFn?!==?activeEffect)?{
????????????effectsToRun.add(effectFn)
????}
??})
??effectsToRun.forEach(effect?=>?effect())
}
復(fù)制代碼
四.computed
computed是vue3中的計算屬性,它可以根據(jù)傳入的參數(shù)進(jìn)行響應(yīng)式的處理:
const?plusOne?=?computed(()?=>?count.value?+?1)
復(fù)制代碼
根據(jù)computed的用法,我們可以知道它的幾個特點:
- 懶執(zhí)行,值變化時才會觸發(fā)
-
緩存功能,如果值沒有變化,就會返回上一次的執(zhí)行結(jié)果 在實現(xiàn)這兩個核心功能之前,我們先來改造一下之前實現(xiàn)的
effect函數(shù)。
怎么能使effect函數(shù)變成懶執(zhí)行呢,比如計算屬性的這種功能,我們不想要他立即執(zhí)行,而是希望在它需要的時候才執(zhí)行。
這時候我們可以在effect函數(shù)中傳遞第二個參數(shù),一個對象,用來設(shè)置一些額外的功能。
function?effect(fn,?options?=?{})?{?//?修改
??let?effectFn?=?function?()?{
????activeEffect?=?effectFn
????effectStack.push(effectFn)
????fn()
????effectStack.pop()
????activeEffect?=?effectStack[effectStack.length?-?1]
??}
??//?只有當(dāng)非lazy的時候才直接執(zhí)行
??if(!options.lazy)?{
????effectFn()
??}
??//?將依賴函數(shù)組為返回值進(jìn)行返回
??return?effectFn?//?新增
}
復(fù)制代碼
這時,如果傳遞了lazy屬性,那么該effect將不會立即執(zhí)行,需要手動進(jìn)行執(zhí)行:
const?effectFn?=?effect(()=>{
??console.log(obj.foo)
},?{?lazy:?true?})
//?手動執(zhí)行
effectFn()
復(fù)制代碼
但是如果我們想要獲取手動執(zhí)行后的值呢,這時只需要在effect函數(shù)中將其返回即可。
function?effect(fn,?options?=?{})?{
??let?effectFn?=?function?()?{
????activeEffect?=?effectFn
????effectStack.push(effectFn)
????//?保存返回值
????const?res?=?fn()?//?新增
????effectStack.pop()
????activeEffect?=?effectStack[effectStack.length?-?1]
????return?res?//?新增
??}
??//?只有當(dāng)非lazy的時候才直接執(zhí)行
??if(!options.lazy)?{
????effectFn()
??}
??//?將依賴函數(shù)組為返回值進(jìn)行返回
??return?effectFn
}
復(fù)制代碼
接下來開始實現(xiàn)computed函數(shù):
function?computed(getter)?{
??//?創(chuàng)建一個可手動調(diào)用的依賴函數(shù)
??const?effectFn?=?effect(getter,?{
????lazy:?true
??})
??//?當(dāng)對象被訪問的時候才調(diào)用依賴函數(shù)
??const?obj?=?{
????get?value()?{
??????return?effectFn()
????}
??}
??return?obj
}
復(fù)制代碼
但是此時還做不到對值進(jìn)行緩存和對比,增加兩個變量,一個存儲執(zhí)行的值,另一個為一個開關(guān),表示“是否可以重新執(zhí)行依賴函數(shù)”:
function?computed(getter)?{
??//?定義value保存執(zhí)行結(jié)果
??//?isRun表示是否需要執(zhí)行依賴函數(shù)
??let?value,?isRun?=?true;?//?新增
??const?effectFn?=?effect(getter,?{
????lazy:?true
??})
??const?obj?=?{
????get?value()?{
??????//?增加判斷,isRun為true時才會重新執(zhí)行
??????if(isRun)?{??//?新增
????????//?保存執(zhí)行結(jié)果
????????value?=?effectFn()?//?新增
????????//?執(zhí)行完畢后再次重置執(zhí)行開關(guān)
????????isRun?=?false?//?新增
??????}
??????return?value
????}
??}
??return?obj
}
復(fù)制代碼
但是上面的實現(xiàn)還有一個問題,就是好像isRun執(zhí)行一次后好像永遠(yuǎn)都不會變成true了,我們的本意是在數(shù)據(jù)發(fā)生變動的時候需要再次觸發(fā)依賴函數(shù),也就是將isRun變?yōu)閠rue,實現(xiàn)這種效果,需要我們?yōu)?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">options再傳遞一個函數(shù),用于用戶自定義的_調(diào)度執(zhí)行_。
function?effect(fn,?options?=?{})?{
??let?effectFn?=?function?()?{
????activeEffect?=?effectFn
????effectStack.push(effectFn)
????const?res?=?fn()?
????effectStack.pop()
????activeEffect?=?effectStack[effectStack.length?-?1]
????return?res?
??}
??//?掛載用戶自定義的調(diào)度執(zhí)行器
??effectFn.options?=?options?//?新增
??if(!options.lazy)?{
????effectFn()
??}
??return?effectFn
}
復(fù)制代碼
接下來需要修改一下trigger如果傳遞了scheduler這個函數(shù),那么只執(zhí)行scheduler這個函數(shù)而不執(zhí)行依賴函數(shù):
function?trigger(target,?key)?{
??let?depsMap?=?store.get(target)
??if?(!depsMap)?return
??const?effects?=?depsMap.get(key)
??let?effectsToRun?=?new?Set()
??effects?&&?effects.forEach(effectFn?=>?{
????if?(effectFn?!==?activeEffect)?{
????????effectsToRun.add(effectFn)
????}
??})
??effectsToRun.forEach(effect?=>?{
????//?如果存在調(diào)度器scheduler,那么直接調(diào)用該調(diào)度器,并將依賴函數(shù)進(jìn)行傳遞
????if(effectFn.options.scheduler)?{?//?新增
??????effectFn.options.scheduler(effect)?//?新增
????}?else?{
??????effect()
????}
??})
}
復(fù)制代碼
那么在computed中就可以實現(xiàn)重置執(zhí)行開關(guān)isRun的操作了:
function?computed(getter)?{
??//?定義value保存執(zhí)行結(jié)果
??//?isRun表示是否需要執(zhí)行依賴函數(shù)
??let?value,?isRun?=?true;?//?新增
??const?effectFn?=?effect(getter,?{
????lazy:?true,
????scheduler()?{
??????if(!isRun)?{
????????isRun?=?true
??????}
????}
??})
??const?obj?=?{
????get?value()?{
??????//?增加判斷,isRun為true時才會重新執(zhí)行
??????if(isRun)?{??//?新增
????????//?保存執(zhí)行結(jié)果
????????value?=?effectFn()?//?新增
????????//?執(zhí)行完畢后再次重置執(zhí)行開關(guān)
????????isRun?=?false?//?新增
??????}
??????return?value
????}
??}
??return?obj
}
復(fù)制代碼
當(dāng)computed傳入的依賴函數(shù)中的值發(fā)生改變時,會觸發(fā)響應(yīng)式對象的trigger函數(shù),而計算屬性創(chuàng)建響應(yīng)式對象時傳入了scheduler,所以當(dāng)數(shù)據(jù)改變時,只會執(zhí)行scheduler函數(shù),在scheduler函數(shù)內(nèi)我們將執(zhí)行開關(guān)重置為true,再下次訪問數(shù)據(jù)觸發(fā)get函數(shù)時,就會重新執(zhí)行依賴函數(shù)。這也就實現(xiàn)了_當(dāng)數(shù)據(jù)發(fā)生改變時,會再次觸發(fā)依賴函數(shù)_的功能了。
為了避免計算屬性被另外一個依賴函數(shù)調(diào)用而失去響應(yīng),我們還需要為計算屬性單獨進(jìn)行綁定響應(yīng)式的功能,形成一個effect嵌套。
function?computed(getter)?{
??let?value,?isRun?=?true;?
??const?effectFn?=?effect(getter,?{
????lazy:?true,
????scheduler()?{
??????if(!isRun)?{
????????isRun?=?true
????????//?當(dāng)計算屬性依賴的響應(yīng)式數(shù)據(jù)發(fā)生變化時,手動調(diào)用trigger函數(shù)觸發(fā)響應(yīng)
????????trigger(obj,?'value')?//?新增
??????}
????}
??})
??const?obj?=?{
????get?value()?{
??????if(isRun)?{?
????????value?=?effectFn()
????????isRun?=?false?
??????}
??????//?當(dāng)讀取value時,手動調(diào)用track函數(shù)進(jìn)行追蹤
??????????track(obj,?'value')
??????return?value
????}
??}
??return?obj
}
復(fù)制代碼
五. watch
先來看一下watch函數(shù)的用法,它的用法也非常簡單:
watch(obj,?()=>{
??console.log(改變了)
})
//?修改數(shù)據(jù),觸發(fā)watch函數(shù)
obj.age++
復(fù)制代碼
watch接受兩個參數(shù),第一個參數(shù)為綁定的響應(yīng)式數(shù)據(jù),第二個參數(shù)為依賴函數(shù),我們依然可以沿用之前的思路來進(jìn)行處理,利用effect以及scheduler來改變觸發(fā)執(zhí)行時機。
function?watch(source,?fn)?{
??effect(
????//?遞歸讀取對象中的每一項,變?yōu)轫憫?yīng)式數(shù)據(jù),綁定依賴函數(shù)
????????()=>?bindData(source),
????{
??????scheduler()?{
????????//?當(dāng)數(shù)據(jù)發(fā)生改變時,調(diào)用依賴函數(shù)
????????fn()
??????}
????}
??)
}
//?readData保存已讀取過的數(shù)據(jù),防止重復(fù)讀取
function?bindData(value,?readData?=?new?Set())?{
??//?此處只考慮對象的情況,如果值已被讀取/值不存在/值不為對象,那么直接返回
??if(typeof?value?!==?'object'?||?value?==?null?||?readData.has(value))?return
??//?保存已讀取對象
??readData.add(value)
??//?遍歷對象
??for(const?key?in?value)?{
????//?遞歸進(jìn)行讀取
????bindData(value[key],?readData)
??}
??return?value
}
復(fù)制代碼
watch函數(shù)還有另外一種用法,就是除了接收對象,還可以接受一個getter函數(shù),例如:
watch(
????()=>?obj.age,
????()=>?{
??????console.log('改變了')
????}?
)
復(fù)制代碼
這種情況下只需要將用戶傳入的getter將我們自定義的bindData替代即可:
function?watch(source,?fn)?{
??let?getter?=?typeof?source?===?'function'???source?:?(()=>?bindData(source))
??effect(
????//?執(zhí)行g(shù)etter
????????()=>?getter(),
????{
??????scheduler()?{
????????//?當(dāng)數(shù)據(jù)發(fā)生改變時,調(diào)用依賴函數(shù)
????????fn()
??????}
????}
??)
}
復(fù)制代碼
其實watch函數(shù)還有一個很重要的功能:就是在用戶傳遞的依賴函數(shù)中可以獲取新值和舊值,但是我們目前還做不到這一點。實現(xiàn)這個功能我們可以配置前文中的lazy屬性來實現(xiàn)。來回顧一下lazy屬性:設(shè)置了lazy之后一開始不會執(zhí)行依賴函數(shù),手動執(zhí)行時會返回執(zhí)行結(jié)果:
function?watch(source,?fn)?{
??let?getter?=?typeof?source?===?'function'???source?:?(()=>?bindData(source))
??//?定義新值與舊值
??let?newVal,?oldVal;?//?新增
??const?effectFn?=?effect(
????//?執(zhí)行g(shù)etter
????????()=>?getter(),
????{
??????lazy:?true,
??????scheduler()?{
????????//?在scheduler重新執(zhí)行依賴函數(shù),得到新值
????????newVal?=?effectFn()?//?新增
????????fn(newVal,?oldVal)?//?新增
????????//?執(zhí)行完畢后更新舊值
????????oldVal?=?newVal?//?新增
??????}
????}
??)
??//?手動調(diào)用依賴函數(shù),取得舊值
??oldVal?=?effectFn()?//?新增
}
復(fù)制代碼
此外,watch函數(shù)還有一個功能,就是可以自定義執(zhí)行時機,比如immediate屬性,他會在創(chuàng)建時立即執(zhí)行一次:
watch(obj,?()=>{
??console.log('改變了')
},?{
??immediate:?true
})
復(fù)制代碼
我們可以把scheduler封裝為一個函數(shù),以便在不同的時機去調(diào)用他:
function?watch(source,?fn,?options?=?{})?{
??let?getter?=?typeof?source?===?'function'???source?:?(()=>?bindData(source))
??let?newVal,?oldVal;?
??const?run?=?()?=>?{?//?新增
????newVal?=?effectFn()
????fn(newVal,?oldVal)
????oldVal?=?newVal
??}
??const?effectFn?=?effect(
????????()=>?getter(),
????{
??????lazy:?true,
??????//?使用run來執(zhí)行依賴函數(shù)
??????scheduler:?run??//?修改
????}
??)
??//?當(dāng)immediate為true時,立即執(zhí)行一次依賴函數(shù)
??if(options.immediate)?{?//?新增
????run()?//?新增
??}?else?{
????oldVal?=?effectFn()?
??}
}
復(fù)制代碼
watch函數(shù)還支持其他的執(zhí)行調(diào)用時機,這里只實現(xiàn)了immediate。
六. 淺響應(yīng)與深響應(yīng)
深響應(yīng)和淺響應(yīng)的區(qū)別:
const?obj?=?reatcive({?foo:?{?bar:?1}?})
effect(()=>{
??console.log(obj.foo.bar)
})
//?修改obj.foo.bar的值,并不能觸發(fā)響應(yīng)
obj.foo.bar?=?2
復(fù)制代碼
因為之前實現(xiàn)的攔截,無論對于什么類型的數(shù)據(jù)都是直接進(jìn)行返回的,如果實現(xiàn)深響應(yīng),那么首先應(yīng)該判斷是否為對象類型的值,如果是對象類型的值,應(yīng)當(dāng)遞歸調(diào)用reactive方法進(jìn)行轉(zhuǎn)換。
//?接收第二個參數(shù),標(biāo)記為是否為淺響應(yīng)
function?createReactive(obj,?isShallow?=?false)?{
??return?new?Proxy(obj,?{
????get(target,?key,?receiver)?{
??????//?訪問raw時,返回原對象
??????if(key?===?'raw')?return?target
??????track(target,?key)
??????const?res?=?Reflect.get(target,?key,?receiver)
??????//?如果是淺響應(yīng),直接返回值
??????if(isShallow)?{
????????return?res
??????}
??????//?判斷res是否為對象并且不為null,循環(huán)調(diào)用reatcive
??????if(typeof?res?===?'object'?&&?res?!==?null)?{
????????return?reatcive(res)
??????}
??????return?res
????},
????//?...省略其他
??})
復(fù)制代碼
將創(chuàng)建響應(yīng)式對象的方法抽離出去,通過傳遞isShallow參數(shù)來決定是否創(chuàng)建深響應(yīng)/淺響應(yīng)對象。
//?深響應(yīng)
function?reactive(obj)?{
??return?createReactive(obj)
}
//?淺響應(yīng)
function?shallowReactive(obj)?{
??return?createReactive(obj,?true)
}
復(fù)制代碼
七. 淺只讀與深只讀
有時候我們并不需要對值進(jìn)行修改,也就是需要值為只讀的,這個操作也分為深只讀和淺只讀,首先需要在createReactive函數(shù)中增加一個參數(shù)isReadOnly,代表是否為只讀屬性。
//?淺只讀
function?shallowReadOnly(obj)?{
??return?createReactive(obj,?true,?true)
}
//?深只讀
function?readOnly(obj)?{
??return?createReactive(obj,?false,?true)
}
復(fù)制代碼
set(target,?key,?newValue,?receiver)?{
??//?是否為只讀屬性,如果是則打印警告信息并直接返回
??if(isReadOnly)?{
????console.log(`屬性${key}是只讀的`)
????return?false
??}
??const?oldVal?=?target[key]
??const?type?=?Object.prototype.hasOwnProperty.call(target,?key)???triggerType.SET?:?triggerType.ADD
??const?res?=?Reflect.set(target,?key,?newValue,?receiver)
??if?(target?===?receiver.raw)?{
????if?(oldVal?!==?newValue?&&?(oldVal?===?oldVal?||?newValue?===?newValue))?{
??????trigger(target,?key,?type)
????}
??}
??return?res
}
復(fù)制代碼
如果為只讀屬性,那么也不需要為其建立響應(yīng)聯(lián)系 如果為只讀屬性,那么在進(jìn)行深層次遍歷的時候,需要調(diào)用readOnly函數(shù)對值進(jìn)行包裝
function?createReactive(obj,?isShallow?=?false,?isReadOnly?=?false)?{
??return?new?Proxy(obj,?{
????get(target,?key,?receiver)?{
??????//?訪問raw時,返回原對象
??????if?(key?===?'raw')?return?target
??????//只有在非只讀的時候才需要建立響應(yīng)聯(lián)系
??????if(!isReadOnly)?{
????????track(target,?key)
??????}
??????const?res?=?Reflect.get(target,?key,?receiver)
??????//?如果是淺響應(yīng),直接返回值
??????if?(isShallow)?{
????????return?res
??????}
??????//?判斷res是否為對象并且不為null,循環(huán)調(diào)用creative
??????if?(typeof?res?===?'object'?&&?res?!==?null)?{
????????//?如果數(shù)據(jù)為只讀,則調(diào)用readOnly對值進(jìn)行包裝
????????return?isReadOnly???readOnly(res)?:?creative(res)
??????}
??????return?res
????},
??})
}
復(fù)制代碼
八. 處理數(shù)組
數(shù)組的索引與length
如果操作數(shù)組時,設(shè)置的索引值大于數(shù)組當(dāng)前的長度,那么要更新數(shù)組的length屬性,所以當(dāng)通過索引設(shè)置元素值時,可能會隱式的修改length的屬性值,因此再j進(jìn)行觸發(fā)響應(yīng)時,也應(yīng)該觸發(fā)與length屬性相關(guān)聯(lián)的副作用函數(shù)重新執(zhí)行。
const?arr?=?reactive(['foo'])?//?數(shù)組原來的長度為1
effect(()=>{
??console.log(arr.length)?//1
})
//?設(shè)置索引為1的值,會導(dǎo)致數(shù)組長度變?yōu)?
arr[1]?=?'bar'
復(fù)制代碼
在判斷操作類型時,新增對數(shù)組類型的判斷,如果代理目標(biāo)是數(shù)組,那么對于操作類型的判斷作出處理:
如果設(shè)置的索引值小于數(shù)組的長度,就視為SET操作,因為他不會改變數(shù)組長度,如果設(shè)置的索引值大于當(dāng)前數(shù)組的長度,那么應(yīng)該被視為ADD操作。
//?定義常量,便于修改
const?triggerType?=?{
??ADD:?'add',
??SET:?'set'
}
set(target,?key,?newValue,?receiver)?{
??if(isReadOnly)?{
????console.log(`屬性${key}是只讀的`)
????return?false
??}
??const?oldVal?=?target[key]
??//?如果目標(biāo)對象是數(shù)組,檢測被設(shè)置的索引值是否小于數(shù)組長度
??const?type?=?Array.isArray(target)?&&?(Number(key)?>?target.length???triggerType.ADD?:?triggerType.SET)
??const?res?=?Reflect.set(target,?key,?newValue,?receiver)
??trigger(target,?key,?type)
??return?res
},
復(fù)制代碼
function?trigger(target,?key,?type)?{
??const?depsMap?=?store.get(target)
??if?(!depsMap)?return
??const?effects?=?depsMap.get(key)
??let?effectsToRun?=?new?Set()
??effects?&&?effects.forEach(effectFn?=>?{
????if?(effectFn?!==?activeEffect)?{
??????effectsToRun.add(effectFn)
????}
??})
??//?當(dāng)操作類型是ADD并且目標(biāo)對象時數(shù)組時,應(yīng)該取出執(zhí)行那些與?length?屬性相關(guān)的副作用函數(shù)
??if(Array.isArray(target)?&&?type?===?triggerType.ADD)?{
????//?取出與length相關(guān)的副作用函數(shù)
????const?lengthEffects?=?deps.get('length')
????lengthEffects?&&?lengthEffects.forEach(effectFn?=>?{
??????if?(effectFn?!==?activeEffect)?{
????????effectsToRun.add(effectFn)
??????}
????})
??}
??effectsToRun.forEach(effect?=>?{
????if?(effectFn.options.scheduler)?{
??????effectFn.options.scheduler(effect)
????}?else?{
??????effect()
????}
??})
}
復(fù)制代碼
還有一點:其實修改數(shù)組的length屬性也會隱式的影響數(shù)組元素:
const?arr?=?reactive(['foo'])
effect(()=>{
??//?訪問數(shù)組的第0個元素
??console.log(arrr[0])?//?foo
})
//?將數(shù)組的長度修改為0,導(dǎo)致第0個元素被刪除,因此應(yīng)該觸發(fā)響應(yīng)
arr.length?=?0
復(fù)制代碼
如上所示,在副作用函數(shù)內(nèi)部訪問了第0個元素,然后將數(shù)組的length屬性修改為0,這回隱式的影響數(shù)組元素,及所有的元素都會被刪除,所以應(yīng)該觸發(fā)副作用函數(shù)重新執(zhí)行。
然而并非所有的對length屬性值的修改都會影響數(shù)組中的已有元素,如果設(shè)置的length屬性為100,這并不會影響第0個元素,當(dāng)修改屬性值時,只有那些索引值大于等于新的length屬性值的元素才需要觸發(fā)響應(yīng)。
調(diào)用trigger函數(shù)時傳入新值:
set(target,?key,?newValue,?receiver)?{
??if(isReadOnly)?{
????console.log(`屬性${key}是只讀的`)
????return?false
??}
??const?oldVal?=?target[key]
??//?如果目標(biāo)對象是數(shù)組,檢測被設(shè)置的索引值是否小于數(shù)組長度
??const?type?=?Array.isArray(target)?&&?(Number(key)?>?target.length???triggerType.ADD?:?triggerType.SET)
??const?res?=?Reflect.set(target,?key,?newValue,?receiver)
??//?將新的值進(jìn)行傳遞,及觸發(fā)響應(yīng)的新值
??trigger(target,?key,?type,?newValue)?//?新增
??return?res
}
復(fù)制代碼
判斷新的下標(biāo)值與需要操作的新的下標(biāo)值進(jìn)行判斷,因為數(shù)組的key為下標(biāo),所以副作用函數(shù)搜集器是以下標(biāo)作為key值的,當(dāng)length發(fā)生變動時,只需要將新值與每個下標(biāo)的key判斷,大于等于新的length值的需要重新執(zhí)行副作用函數(shù)。
未命名繪圖.drawio_(2)_1659679803962_0.png如上圖所示,Map為根據(jù)數(shù)組的key,也就是id組成的Map結(jié)構(gòu),他們的每一個key都對應(yīng)一個Set,用于保存這個key下面的所有的依賴函數(shù)。
當(dāng)length屬性發(fā)生變動時,應(yīng)當(dāng)取出所有key值大于等于length值的所有依賴函數(shù)進(jìn)行執(zhí)行。
function?trigger(target,?key,?type,?newValue)?{
??const?depsMap?=?store.get(target)
??if?(!depsMap)?return
??const?effects?=?depsMap.get(key)
??let?effectsToRun?=?new?Set()
??effects?&&?effects.forEach(effectFn?=>?{
????if?(effectFn?!==?activeEffect)?{
??????effectsToRun.add(effectFn)
????}
??})
??//?如果操作目標(biāo)是數(shù)組,并且修改了數(shù)組的length屬性
??if(Array.isArray(target)?&&?key?===?'length')?{
????//?對于索引值大于或等于新的length元素
????//?需要把所有相關(guān)聯(lián)的副作用函數(shù)取出并添加到effectToRun中待執(zhí)行
????depsMap.forEach((effects,?key)=>{
??????//?key?與?newValue均為數(shù)組下標(biāo),因為數(shù)組中key為index
??????if(key?>=?newValue)?{
????????effects.forEach(effectFn=>{
??????????if?(effectFn?!==?activeEffect)?{
????????????effectsToRun.add(effectFn)
??????????}
????????})
??????}
????})
??}
??//?...省略
}
復(fù)制代碼
本文的實現(xiàn)數(shù)組這種數(shù)據(jù)結(jié)構(gòu)只考慮了針對長度發(fā)生變化的情況。
九. ref
由于Proxy的代理目標(biāo)是非原始值,所以沒有任何手段去攔截對原始值的操作:
let?str?=?'hi'
//?無法攔截對值的修改
str?=?'pino'
復(fù)制代碼
解決方法是:使用一個非原始值去包裹原始值:
function?ref(val)?{
??//?創(chuàng)建一個對象對原始值進(jìn)行包裹
??const?wrapper?=?{
????value:?val
??}
??//?使用reactive函數(shù)將包裹對象編程響應(yīng)式數(shù)據(jù)并返回
??return?reactive(wrapper)
}
復(fù)制代碼
如何判斷是用戶傳入的對象還是包裹對象呢?
const?ref1?=?ref(1)
const?ref2?=?reactive({?value:?1?})
復(fù)制代碼
只需要在包裹對象內(nèi)部定義一個不可枚舉且不可寫的屬性:
function?ref(val)?{
??//?創(chuàng)建一個對象對原始值進(jìn)行包裹
??const?wrapper?=?{
????value:?val
??}
??//?定義一個屬性值__v_isRef,值為true,代表是包裹對象
??Object.defineProperty(wrapper,?'_isRef',?{
????value:?true
??})
??//?使用reactive函數(shù)將包裹對象編程響應(yīng)式數(shù)據(jù)并返回
??return?reactive(wrapper)
}
復(fù)制代碼
十. 響應(yīng)丟失問題與toRefs
在使用...解構(gòu)賦值時會導(dǎo)致響應(yīng)式丟失:
const?obj?=?reactive({?foo:?1,?bar:?2?})
//?將響應(yīng)式數(shù)據(jù)展開到一個新的對象newObj
const?newObj?=?{
??...obj
}
//?此時相當(dāng)于:
const?newObj?=?{
??foo:?1,
??bar:?2
}
effect(()=>{
??//在副作用函數(shù)中通過新對象newObj讀取foo屬性值
??console.log(newObj.foo)
})
//?obj,foo并不會觸發(fā)響應(yīng)
obj.foo?=?100
復(fù)制代碼
首先創(chuàng)建一個響應(yīng)式對象obj,然后使用展開運算符得到一個新對象newObj,他是一個普通對象,不具有響應(yīng)式的能力,所以修改obj.foo的值不會觸發(fā)副作用函數(shù)重新更新。
解決方法:
const?newObj?=?{
??foo:?{
????//?用于返回其原始的響應(yīng)式對象
????get?value()?{
??????return?obj.foo
????}
??},
??bar:?{
????get?value()?{
??????return?obj.bar
????}
??}
}
復(fù)制代碼
將單個值包裝為一個對象,相當(dāng)于訪問該屬性的時候會得到該屬性的getter,在getter中返回原始的響應(yīng)式對象。
相當(dāng)于解構(gòu)訪問newObj.foo === obj.foo。
{
??get?value()?{
????return?obj.foo
??}
}
復(fù)制代碼
toRefs
function?toRefs(obj)?{
??let?res?=?{}
??//?處理整個對象時,將屬性依次進(jìn)行遍歷,調(diào)用toRef進(jìn)行轉(zhuǎn)化
??for(let?key?in?obj)?{
????res[key]?=?toRef(obj,?key)
??}
??return?res
}
function?toRef(obj,?key)?{
??const?wrapper?=?{
????//?允許讀取值
????get?value()?{
??????return?obj[key]
????},
????//?允許設(shè)置值
????set?value(val)?{
??????obj[key]?=?val
????}
??}
??//?標(biāo)志為ref對象
??Object.defineProperty(wrapper,?'_isRef',?{
????value:?true
??})
??return?wrapper
}
復(fù)制代碼
使用toRefs處理整個對象,在toRefs這個函數(shù)中循環(huán)處理了對象所包含的所有屬性。
??const?newObj?=?{?...toRefs(obj)?}
復(fù)制代碼
當(dāng)設(shè)置value屬性值的時候,最終設(shè)置的是響應(yīng)式數(shù)據(jù)的同名屬性值。
一個基本的vue3響應(yīng)式就完成了,但是本文所實現(xiàn)的依然是閹割版本,有很多情況都沒有進(jìn)行考慮,還有好多功能沒有實現(xiàn),比如:攔截 Map,Set,數(shù)組的其他問題,對象的其他問題,其他api的實現(xiàn),但是上面的實現(xiàn)已經(jīng)足夠讓你理解vue3響應(yīng)式原理實現(xiàn)的核心了,這里還有很多其他的資料需要推薦,比如阮一峰老師的es6教程,對于vue3底層原理的實現(xiàn),許多知識依然是需要回顧和復(fù)習(xí),查看原始底層的實現(xiàn),再比如霍春陽老師的《vue.js的設(shè)計與實現(xiàn)》這本書,這本書目前我也只看完了一半,但是截止到目前我認(rèn)為這本書對于學(xué)習(xí)vue3的原理是非常深入淺出,鞭辟入里的,本文的許多例子也是借鑒了這本書。
最后當(dāng)然是需要取讀一讀源碼,不過在讀源碼之前能夠先了解一下實現(xiàn)的核心原理,再去看源碼是事半功倍的。希望大家都能早日學(xué)透源碼,面試的時候能夠?qū)Υ鹑缌?,工作中遇到的問題也能從原理層面去理解和更好地解決!
目前我也在實現(xiàn)一個mini-vue,截止到目前只實現(xiàn)了響應(yīng)式部分,而且與本文的實現(xiàn)方式有所不同,后續(xù)還會繼續(xù)實現(xiàn)編譯和虛擬DOM部分,歡迎star!??
k-vue [4]
如果想學(xué)習(xí)《vue.js的設(shè)計與實現(xiàn)》這本書這本書,那么請關(guān)注下面這個鏈接??作為參考,里面包含了根據(jù)具體的問題的功能進(jìn)行拆分實現(xiàn),同樣也只實現(xiàn)了響應(yīng)式的部分!
vue3-analysis [5]
實現(xiàn)一個mini-vue系列文章
超詳細(xì)整理vue3基礎(chǔ)知識?? [6]
寫在最后??
未來可能會更新實現(xiàn)mini-vue3和javascript基礎(chǔ)知識系列,希望能一直堅持下去,期待多多點贊????,一起進(jìn)步!????
https://juejin.cn/post/7129644396533776420
祝 您:2022 年暴富!萬事如意!
點贊和在看就是最大的支持,
比心??
