本篇文章帶你深度剖析vue3響應(yīng)式(附腦圖),本文的目標(biāo)是實(shí)現(xiàn)一個(gè)基本的vue3的響應(yīng)式,包含最基礎(chǔ)的情況的處理。
本文你將學(xué)到
- 一個(gè)基礎(chǔ)的響應(yīng)式實(shí)現(xiàn) ✅
- Proxy ✅
- Reflect ✅
- 嵌套effect的實(shí)現(xiàn) ✅
- computed ✅
- watch ✅
- 淺響應(yīng)與深響應(yīng) ✅
- 淺只讀與深只讀 ✅
- 處理數(shù)組長度 ✅
- ref ✅
- toRefs ✅
一. 實(shí)現(xiàn)一個(gè)完善的響應(yīng)式
所謂的響應(yīng)式數(shù)據(jù)的概念,其實(shí)最主要的目的就是為數(shù)據(jù)綁定執(zhí)行函數(shù),當(dāng)數(shù)據(jù)發(fā)生變動(dòng)的時(shí)候,再次觸發(fā)函數(shù)的執(zhí)行。(學(xué)習(xí)視頻分享:vue視頻教程)
例如我們有一個(gè)對象data
,我們想讓它變成一個(gè)響應(yīng)式數(shù)據(jù),當(dāng)data
的數(shù)據(jù)發(fā)生變化時(shí),自動(dòng)執(zhí)行effect
函數(shù),使nextVal
變量的值也進(jìn)行變化:
// 定義一個(gè)對象 let data = { name: 'pino', age: 18 } let nextVal // 待綁定函數(shù) function effect() { nextVal = data.age + 1 } data.age++
上面的例子中我們將data
中的age
的值進(jìn)行變化,但是effect
函數(shù)并沒有執(zhí)行,因?yàn)楝F(xiàn)在effect
函數(shù)與data
這個(gè)對象不能說是沒啥聯(lián)系,簡直就是半毛錢的關(guān)系都沒有。
那么怎么才能使這兩個(gè)毫不相關(guān)的函數(shù)與對象之間產(chǎn)生關(guān)聯(lián)呢?
因?yàn)橐粋€(gè)對象最好可以綁定多個(gè)函數(shù),所以有沒有可能我們?yōu)?code>data這個(gè)對象定義一個(gè)空間,每當(dāng)data
的值進(jìn)行變化的時(shí)候就會執(zhí)行這個(gè)空間里的函數(shù)?
答案是有的。
1. Object.defineProperty()
js在原生提供了一個(gè)用于操作對象的比較底層的api:Object.defineProperty()
,它賦予了我們對一個(gè)對象的讀取和攔截的操作。
Object.defineProperty()
方法直接在一個(gè)對象上定義一個(gè)新屬性,或者修改一個(gè)已經(jīng)存在的屬性, 并返回這個(gè)對象。
Object.defineProperty(obj, prop, descriptor)
參數(shù)
obj
需要定義屬性的對象。 prop
需被定義或修改的屬性名。 descriptor
(描述符) 需被定義或修改的屬性的描述符。
其中descriptor
接受一個(gè)對象,對象中可以定義以下的屬性描述符,使用屬性描述符對一個(gè)對象進(jìn)行攔截和控制:
-
value
——當(dāng)試圖獲取屬性時(shí)所返回的值。 -
writable
——該屬性是否可寫。 -
enumerable
——該屬性在for in循環(huán)中是否會被枚舉。 -
configurable
——該屬性是否可被刪除。 -
set()
——該屬性的更新操作所調(diào)用的函數(shù)。 -
get()
——獲取屬性值時(shí)所調(diào)用的函數(shù)。
另外,數(shù)據(jù)描述符(其中屬性為: enumerable
, configurable
, value
, writable
)與存取描述符(其中屬性為 enumerable
, configurable
, set()
, get()
)之間是有互斥關(guān)系的。在定義了 set()
和 get()
之后,描述符會認(rèn)為存取操作已被 定義了,其中再定義 value
和 writable
會引起錯(cuò)誤。
let obj = { name: "小花" } Object.defineProperty(obj, 'name', { // 屬性讀取時(shí)進(jìn)行攔截 get() { return '小明'; }, // 屬性設(shè)置時(shí)攔截 set(newValue) { obj.name = newValue; }, enumerable: true, configurable: true });
上面的例子中就已經(jīng)完成對一個(gè)對象的最基本的攔截,這也是vue2.x
中對對象監(jiān)聽的方式,但是由于Object.defineProperty()
中存在一些問題,例如:
-
一次只能對一個(gè)屬性進(jìn)行監(jiān)聽,需要遍歷來對所有屬性監(jiān)聽
-
對于對象的新增屬性,需要手動(dòng)監(jiān)聽
-
對于數(shù)組通過
push
、unshift
方法增加的元素,也無法監(jiān)聽
那么vue3
版本中是如何對一個(gè)對象進(jìn)行攔截的呢?答案是es6
中的Proxy
。
由于本文主要是vue3
版本的響應(yīng)式的實(shí)現(xiàn),如果想要深入了解Object.defineProperty()
,請移步:MDN Object.defineProperty
2. Proxy
proxy
是es6
版本出現(xiàn)的一種對對象的操作方式,Proxy
可以理解成,在目標(biāo)對象之前架設(shè)一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機(jī)制,可以對外界的訪問進(jìn)行過濾和改寫。Proxy
這個(gè)詞的原意是代理,用在這里表示由它來“代理”某些操作,可以譯為“代理器”。
通過proxy
我們可以實(shí)現(xiàn)對一個(gè)對象的讀取,設(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
,返回一個(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)對象所有自身的屬性的屬性名,而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)
,返回一個(gè)布爾值。 -
preventExtensions(target) :攔截
Object.preventExtensions(proxy)
,返回一個(gè)布爾值。 -
getPrototypeOf(target) :攔截
Object.getPrototypeOf(proxy)
,返回一個(gè)對象。 -
isExtensible(target) :攔截
Object.isExtensible(proxy)
,返回一個(gè)布爾值。 -
setPrototypeOf(target, proto) :攔截
Object.setPrototypeOf(proxy, proto)
,返回一個(gè)布爾值。如果目標(biāo)對象是函數(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)
。
如果想要詳細(xì)了解proxy
,請移步:es6.ruanyifeng.com/#docs/proxy…
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 } });
3. 一個(gè)最簡單的響應(yīng)式
有了proxy
,我們就可以根據(jù)之前的思路實(shí)現(xiàn)一個(gè)基本的響應(yīng)式功能了,我們的思路是這樣的:在對象被讀取時(shí)把函數(shù)收集到一個(gè)“倉庫”,在對象的值被設(shè)置時(shí)觸發(fā)倉庫中的函數(shù)。
由此我們可以寫出一個(gè)最基本的響應(yīng)式功能:
// 定義一個(gè)“倉庫”,用于存儲觸發(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()) } })
我們創(chuàng)建了一個(gè)用于保存依賴函數(shù)的“倉庫”,它是Set
類型,然后使用proxy
對對象data
進(jìn)行代理,設(shè)置了set
和get
攔截函數(shù),用于攔截讀取和設(shè)置操作,當(dāng)讀取屬性時(shí),將依賴函數(shù)effect
存儲到“倉庫”中,當(dāng)設(shè)置屬性值時(shí),將依賴函數(shù)從“倉庫”中取出并重新執(zhí)行。
還有一個(gè)小問題,怎么觸發(fā)對象的讀取操作呢?我們可以直接調(diào)用一次effect
函數(shù),如果在effect
函數(shù)中存在需要收集的屬性,那么執(zhí)行一次effect
函數(shù)也是比較符合常理的。
// 定義一個(gè)對象 let data = { name: 'pino', age: 18 } let nextVal // 待綁定函數(shù) function effect() { // 依賴函數(shù)在這里被收集 // 當(dāng)調(diào)用data.age時(shí),effect函數(shù)被收集到“倉庫”中 nextVal = data.age + 1 console.log(nextVal) } // 執(zhí)行依賴函數(shù) effect() // 19 setTimeout(()=>{ // 使用proxy進(jìn)行代理后,使用代理后的對象名 // 觸發(fā)設(shè)置操作,此時(shí)會取出effect函數(shù)進(jìn)行執(zhí)行 data_proxy.age++ // 2秒后輸出 20 }, 2000)
一開始會執(zhí)行一次effect
,然后函數(shù)兩秒鐘后會執(zhí)行代理對象設(shè)置操作,再次執(zhí)行effect
函數(shù),輸出20。
此時(shí)整個(gè)響應(yīng)式流程的功能是這樣的:
階段一,在屬性被讀取時(shí),為對象屬性收集依賴函數(shù):
階段二,當(dāng)屬性發(fā)生改變時(shí),再次觸發(fā)依賴函數(shù)
這樣就實(shí)現(xiàn)了一個(gè)最基本的響應(yīng)式的功能。
4. 完善
問題一
其實(shí)上面實(shí)現(xiàn)的功能還有很大的缺陷,首先最明顯的問題是,我們把effect
函數(shù)給固定了,如果用戶使用的依賴函數(shù)不叫effect
怎么辦,顯然我們的功能就不能正常運(yùn)行了。
所以先來進(jìn)行第一步的優(yōu)化:抽離出一個(gè)公共方法,依賴函數(shù)由用戶來傳遞參數(shù)。
我們使用effect
函數(shù)來接受用戶傳遞的依賴函數(shù):
// effect接受一個(gè)函數(shù),把這個(gè)匿名函數(shù)當(dāng)作依賴函數(shù) function effect(fn) { // 執(zhí)行依賴函數(shù) fn() } // 使用 effect(()=>{ nextVal = data.age + 1 console.log(nextVal) })
但是effect
函數(shù)內(nèi)部只是執(zhí)行了,在get
函數(shù)中怎么能知道用戶傳遞的依賴函數(shù)是什么呢,這兩個(gè)操作并不在一個(gè)函數(shù)內(nèi)啊?其實(shí)可以使用一個(gè)全局變量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] },
調(diào)用effect
函數(shù)傳遞一個(gè)匿名函數(shù)作為依賴函數(shù),當(dāng)執(zhí)行時(shí),首先會把匿名函數(shù)賦值給全局變量activeEffect
,然后觸發(fā)屬性的讀取操作,進(jìn)而觸發(fā)get
攔截,將全局變量activeEffect
進(jìn)行收集。
問題二
從上面我們定義的對象可以看到,我們的對象data
中有兩個(gè)屬性,上面的例子中我們只給age
建立了響應(yīng)式連接,那么如果我現(xiàn)在也想給name
建立響應(yīng)式連接怎么辦呢?那好說,那我們直接向“倉庫”中繼續(xù)添加依賴函數(shù)不就行了嗎。
其實(shí)這會帶來很嚴(yán)重的問題,由于 “倉庫”并沒有與被操作的目標(biāo)屬性之間建立聯(lián)系,而上面我們的實(shí)現(xiàn)只是將整個(gè)“倉庫”遍歷了一遍,所以無論哪個(gè)屬性被觸發(fā),都會將“倉庫”中所有的依賴函數(shù)都取出來執(zhí)行一遍,因?yàn)檎麄€(gè)執(zhí)行程序中可能有很多對象及屬性都設(shè)置了響應(yīng)式聯(lián)系,這將會帶來很大的性能浪費(fèi)。所謂牽一發(fā)而動(dòng)全身,這種結(jié)果顯然不是我們想要的。
let data = { name: 'pino', age: 18 }
所以我們要重新設(shè)計(jì)一下“倉庫”的數(shù)據(jù)結(jié)構(gòu),目的就是為了可以在屬性這個(gè)粒度下和“倉庫”建立明確的聯(lián)系。
就拿我們上面進(jìn)行操作的對象來說,存在著兩層的結(jié)構(gòu),有兩個(gè)角色,對象data
以及屬性name``age
let data = { name: 'pino', age: 18 }
他們的關(guān)系是這樣的:
data -> name -> effectFn // 如果兩個(gè)屬性讀取了同一個(gè)依賴函數(shù) data -> name -> effectFn -> age -> effectFn // 如果兩個(gè)屬性讀取了不同的依賴函數(shù) data -> name -> effectFn -> age -> effectFn1 // 如果是兩個(gè)不同的對象 data -> name -> effectFn -> age -> effectFn1 data2 -> addr -> effectFn
接下來我們實(shí)現(xiàn)一下代碼,為了方便調(diào)用,將設(shè)置響應(yīng)式數(shù)據(jù)的操作封裝為一個(gè)函數(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) { } }); }
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()); }
我們將讀取操作封裝為了函數(shù)track
,觸發(fā)依賴函數(shù)的動(dòng)作封裝為了trigger
方便調(diào)用,現(xiàn)在的整個(gè)“倉庫”結(jié)構(gòu)是這樣的:
WeakMap
可能有人會問了,為什么設(shè)置“倉庫”要使用WeakMap
呢,我使用一個(gè)普通對象來創(chuàng)建不行嗎? –
WeakMap
結(jié)構(gòu)與 Map
結(jié)構(gòu)類似,也是用于生成鍵值對的集合。
WeakMap
與 Map
的區(qū)別有兩點(diǎn)。
首先, 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
上面代碼中,如果將數(shù)值 1
和 Symbol
值作為 WeakMap 的鍵名,都會報(bào)錯(cuò)。
其次, WeakMap
的鍵名所指向的對象,不計(jì)入垃圾回收機(jī)制。
WeakMap
的設(shè)計(jì)目的在于,有時(shí)我們想在某個(gè)對象上面存放一些數(shù)據(jù),但是這會形成對于這個(gè)對象的引用。請看下面的例子。
const e1 = document.getElementById('foo'); const e2 = document.getElementById('bar'); const arr = [ [e1, 'foo 元素'], [e2, 'bar 元素'], ];
上面代碼中, e1
和 e2
是兩個(gè)對象,我們通過 arr
數(shù)組對這兩個(gè)對象添加一些文字說明。這就形成了 arr
對 e1
和 e2
的引用。
一旦不再需要這兩個(gè)對象,我們就必須手動(dòng)刪除這個(gè)引用,否則垃圾回收機(jī)制就不會釋放 e1
和 e2
占用的內(nèi)存。
// 不需要 e1 和 e2 的時(shí)候 // 必須手動(dòng)刪除引用 arr [0] = null; arr [1] = null;
上面這樣的寫法顯然很不方便。一旦忘了寫,就會造成內(nèi)存泄露。
它的鍵名所引用的對象都是弱引用,即垃圾回收機(jī)制不將該引用考慮在內(nèi)。因此,只要所引用的對象的其他引用都被清除,垃圾回收機(jī)制就會釋放該對象所占用的內(nèi)存。也就是說,一旦不再需要,WeakMap
里面的鍵名對象和所對應(yīng)的鍵值對會自動(dòng)消失,不用手動(dòng)刪除引用。
如果我們上文中target
對象沒有任何引用了,那么說明用戶已經(jīng)不需要用到它了,這時(shí)垃圾回收器會自動(dòng)執(zhí)行回收,而如果使用Map
來進(jìn)行收集,那么即使其他地方的代碼已經(jīng)對target
沒有任何引用,這個(gè)target
也不會被回收。
Reflect
在vue3中的實(shí)現(xiàn)方式和我們的基本實(shí)現(xiàn)還有一點(diǎ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) } }) }
那么為什么要使用Reflect
來操作數(shù)據(jù)呢,像之前一樣直接操作原對象不行嗎,我們先來看一下一種特殊的情況:
const obj = { foo: 1, get bar() { return this.foo } }
在effect
依賴函數(shù)中通過代理對象p訪問bar屬性:
effect(()=>{ console.log(p.bar) // 1 })
可以分析一下這個(gè)過程發(fā)生了什么,當(dāng)effect
函數(shù)被調(diào)用時(shí),會讀取p.bar
屬性,他發(fā)現(xiàn)p.bar
屬性是一個(gè)訪問器屬性,因此會執(zhí)行getter
函數(shù),由于在getter
函數(shù)中通過this.foo
讀取了foo
屬性的值,因此我們會認(rèn)為副作用函數(shù)與屬性foo
之間也會建立聯(lián)系,當(dāng)修改p.foo
的值的時(shí)候因該也能夠觸發(fā)響應(yīng),使依賴函數(shù)重新執(zhí)行才對,然而當(dāng)修改p.foo
的時(shí)候,并沒有觸發(fā)依賴函數(shù):
p.foo++
實(shí)際上問題就出在bar
屬性中的訪問器函數(shù)getter
上:
get bar() { // 這個(gè)this究竟指向誰? return this.foo }
當(dāng)通過代理對象p訪問p.bar
,這回觸發(fā)代理對象的get
攔截函數(shù)執(zhí)行:
const p = new Proxt(obj, { get(target, key) { track(target, key) return target[key] } })
可以看到在get
的攔截函數(shù)中,通過target[key]
返回屬性值,其中target
是原始對象obj
,而key
就是字符串'bar'
,所以target[key]
就相當(dāng)于obj.bar
。因此當(dāng)我們使用p.bar
訪問bar
屬性時(shí),他的getter
函數(shù)內(nèi)的this
其實(shí)指向原始對象obj
,這說明我們最終訪問的是obj.foo
。所以在依賴函數(shù)內(nèi)部通過原始對象訪問他的某個(gè)屬性是不會建立響應(yīng)聯(lián)系的:
effect(()=>{ // obj是原始數(shù)據(jù),不是代理對象,不會建立響應(yīng)聯(lián)系 obj.foo })
那么怎么解決這個(gè)問題呢,這時(shí)候就需要用到 Reflect
出場了。
先來看一下Reflect
是啥:
Reflect
函數(shù)的功能就是提供了訪問一個(gè)對象屬性的默認(rèn)行為,例如下面兩個(gè)操作是等價(jià)的:
const obj = { foo: 1 } // 直接讀取 console.log(obj.foo) //1 // 使用Reflect.get讀取 console.log(Reflect.get(obj, 'foo')) // 1
實(shí)際上Reflect.get
函數(shù)還能接受第三個(gè)函數(shù),即制定接受者receiver
,可以把它理解為函數(shù)調(diào)用過程中的this
:
const obj = { foo: 1 } console.log(Reflect.get(obj, 'foo', { foo: 2 })) // 輸出的是 2 而不是 1
在這段代碼中,指定了第三個(gè)參數(shù)receiver為一個(gè)對象{ foo: 2 }
,這是讀取到的值時(shí)receiver
對象的foo
屬性。
而我們上文中的問題的解決方法就是在操作對象數(shù)據(jù)的時(shí)候通過Reflect
的方法來傳遞第三個(gè)參數(shù)receiver
,它代表誰在讀取屬性:
const p = new Proxt(obj, { // 讀取屬性接收receiver get(target, key, receiver) { track(target, key) // 使用Reflect.get返回讀取到的屬性值 return Reflect.get(target, key, receiver) } })
當(dāng)使用代理對象p
訪問bar
屬性時(shí),那么receiver
就是p,可以把它理解為函數(shù)調(diào)用中的this
。
所以我們改造一下reactive
函數(shù)的實(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) } }) }
擴(kuò)展
Proxy -> get()
get
方法用于攔截某個(gè)屬性的讀取操作,可以接受三個(gè)參數(shù),依次為目標(biāo)對象、屬性名和 proxy
(代理) 實(shí)例本身(嚴(yán)格地說,是操作行為所針對的對象),其中最后一個(gè)參數(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
如果 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
如果第一個(gè)參數(shù)不是對象, Reflect.get
方法會報(bào)錯(cuò)。
Reflect.get(1, 'foo') // 報(bào)錯(cuò) Reflect.get(false, 'foo') // 報(bào)錯(cuò)
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
如果 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
注意,如果 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
上面代碼中, Proxy.set
攔截里面使用了 Reflect.set
,而且傳入了 receiver
,導(dǎo)致觸發(fā) Proxy.defineProperty
攔截。這是因?yàn)?Proxy.set
的 receiver
參數(shù)總是指向當(dāng)前的 Proxy
實(shí)例(即上例的 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
如果第一個(gè)參數(shù)不是對象, Reflect.set
會報(bào)錯(cuò)。
Reflect.set(1, 'foo', {}) // 報(bào)錯(cuò) Reflect.set(false, 'foo', {}) // 報(bào)錯(cuò)
到這里,一個(gè)非?;镜捻憫?yīng)式的功能就完成了,整體代碼如下:
// 定義倉庫 let store = new WeakMap() // 定義當(dāng)前處理的依賴函數(shù) let activeEffect function effect(fn) { // 將操作包裝為一個(gè)函數(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)建一個(gè)新的Set來進(jìn)行執(zhí)行依賴函數(shù) let effectsToRun = new Set() effects && effects.forEach(effectFn => { effectsToRun.add(effectFn) }) effectsToRun.forEach(effect => effect()) }
二. 嵌套effect
在日常的工作中,effect
函數(shù)并不是單獨(dú)存在的,比如在vue的渲染函數(shù)中,各個(gè)組件之間互相嵌套,那么他們在組件中所使用的effect
是必然會發(fā)生嵌套的:
effect(function effectFn1() { effect(function effectFn1() { // ... }) })
當(dāng)組件中發(fā)生嵌套時(shí),此時(shí)的渲染函數(shù):
effect(()=>{ Father.render() //嵌套子組件 effect(()=>{ Son.render() }) })
但是此時(shí)我們實(shí)現(xiàn)的effect
并沒有這個(gè)能力,執(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 })
在這段代碼中,定義了代理對象obj
,里面有兩個(gè)屬性foo
和bar
,然后定義了收集foo
的依賴函數(shù),在依賴函數(shù)的內(nèi)部又定義了bar
的依賴函數(shù)。 在理想狀態(tài)下,我們希望依賴函數(shù)與屬性之間的關(guān)系如下:
obj -> foo -> effect1 -> bar -> effect2
當(dāng)修改obj.foo
的值的時(shí)候,會觸發(fā)effect1
函數(shù)執(zhí)行,由于effect2
函數(shù)在effect
函數(shù)內(nèi)部,所以effect2
函數(shù)也會執(zhí)行,而當(dāng)修改obj.bar
時(shí),只會觸發(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 = '前來買瓜'
看一下執(zhí)行結(jié)果:
可以看到effect2
函數(shù)竟然執(zhí)行了兩次?按照之前的分析,當(dāng)obj.foo
被修改后,應(yīng)當(dāng)觸發(fā)effect1
這個(gè)依賴函數(shù),但是為什么會effect2
會被再次執(zhí)行呢? 來看一下我們effect
函數(shù)的實(shí)現(xiàn):
function effect(fn) { // 將依賴函數(shù)進(jìn)行包裝 const effectFn = ()=> { activeEffect = effectFn fn() } effectFn() }
其實(shí)在這里就已經(jīng)很容易看出問題了,在接受用戶傳遞過來的值時(shí),我們直接將activeEffect
這個(gè)全局變量進(jìn)行了覆蓋!所以在內(nèi)部執(zhí)行完后,activeEffect
這個(gè)變量就已經(jīng)是effect2
函數(shù)了,而且永遠(yuǎn)不會再次變?yōu)?code>effect1,此時(shí)再進(jìn)行收集依賴函數(shù)時(shí),永遠(yuǎn)收集的都是effect2
函數(shù)。
那么如何解決這種問題呢,這種情況可以借鑒棧結(jié)構(gòu)來進(jìn)行處理,棧結(jié)構(gòu)是一種后進(jìn)先出的結(jié)構(gòu),在依賴函數(shù)執(zhí)行時(shí),將當(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ù)的時(shí)候進(jìn)行g(shù)et收集 fn() // 收集完畢后彈出 effectStack.pop() // 新增 // 始終指向棧頂 activeEffect = effectStack[effectStack.length - 1] // 新增 } effectFn() }
此時(shí)兩個(gè)屬性所對應(yīng)的依賴函數(shù)便不會發(fā)生錯(cuò)亂了。
三. 避免無限循環(huán)
如果現(xiàn)在將effect
函數(shù)中傳遞的依賴函數(shù)改一下:
// 定義一個(gè)對象 let data = { name: 'pino', age: 18 } // 將data更改為響應(yīng)式對象 let obj = reactive(data) effect(() => { obj.age++ })
在這段代碼中,我們將代理對象obj
的age
屬性執(zhí)行自增操作,但是執(zhí)行這段代碼,卻發(fā)現(xiàn)竟然棧溢出了?這是怎么回事呢?
其實(shí)在effect
中處理依賴函數(shù)時(shí),obj.age++
的操作其實(shí)可以看做是這樣的:
effect(()=>{ // 等式右邊的操作是先執(zhí)行了一次讀取操作 obj.age = obj.age + 1 })
這段代碼的執(zhí)行流程是這樣的:首先讀取obj.foo
的值,這會觸發(fā)track
函數(shù)進(jìn)行收集操作,也就是將當(dāng)前的依賴函數(shù)收集到“倉庫”中,接著將其加1后再賦值給obj.foo
,此時(shí)會觸發(fā)trigger
操作,即把“倉庫”中的依賴函數(shù)取出并執(zhí)行。但是此時(shí)該依賴函數(shù)正在執(zhí)行中,還沒有執(zhí)行完就要再次開始下一次的執(zhí)行。就會導(dǎo)致無限的遞歸調(diào)用自己。
解決這個(gè)問題,其實(shí)只需要在觸發(fā)函數(shù)執(zhí)行時(shí),判斷當(dāng)前取出的依賴函數(shù)是否等于activeEffect
,就可以避免重復(fù)執(zhí)行同一個(gè)依賴函數(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ù) // 因?yàn)閯h除又添加都在同一個(gè)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()) }
四.computed
computed
是vue3中的計(jì)算屬性,它可以根據(jù)傳入的參數(shù)進(jìn)行響應(yīng)式的處理:
const plusOne = computed(() => count.value + 1)
根據(jù)computed
的用法,我們可以知道它的幾個(gè)特點(diǎn):
-
懶執(zhí)行,值變化時(shí)才會觸發(fā)
-
緩存功能,如果值沒有變化,就會返回上一次的執(zhí)行結(jié)果 在實(shí)現(xiàn)這兩個(gè)核心功能之前,我們先來改造一下之前實(shí)現(xiàn)的
effect
函數(shù)。
怎么能使effect
函數(shù)變成懶執(zhí)行呢,比如計(jì)算屬性的這種功能,我們不想要他立即執(zhí)行,而是希望在它需要的時(shí)候才執(zhí)行。
這時(shí)候我們可以在effect
函數(shù)中傳遞第二個(gè)參數(shù),一個(gè)對象,用來設(shè)置一些額外的功能。
function effect(fn, options = {}) { // 修改 let effectFn = function () { activeEffect = effectFn effectStack.push(effectFn) fn() effectStack.pop() activeEffect = effectStack[effectStack.length - 1] } // 只有當(dāng)非lazy的時(shí)候才直接執(zhí)行 if(!options.lazy) { effectFn() } // 將依賴函數(shù)組為返回值進(jìn)行返回 return effectFn // 新增 }
這時(shí),如果傳遞了lazy
屬性,那么該effect
將不會立即執(zhí)行,需要手動(dòng)進(jìn)行執(zhí)行:
const effectFn = effect(()=>{ console.log(obj.foo) }, { lazy: true }) // 手動(dòng)執(zhí)行 effectFn()
但是如果我們想要獲取手動(dòng)執(zhí)行后的值呢,這時(shí)只需要在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的時(shí)候才直接執(zhí)行 if(!options.lazy) { effectFn() } // 將依賴函數(shù)組為返回值進(jìn)行返回 return effectFn }
接下來開始實(shí)現(xiàn)computed
函數(shù):
function computed(getter) { // 創(chuàng)建一個(gè)可手動(dòng)調(diào)用的依賴函數(shù) const effectFn = effect(getter, { lazy: true }) // 當(dāng)對象被訪問的時(shí)候才調(diào)用依賴函數(shù) const obj = { get value() { return effectFn() } } return obj }
但是此時(shí)還做不到對值進(jìn)行緩存和對比,增加兩個(gè)變量,一個(gè)存儲執(zhí)行的值,另一個(gè)為一個(gè)開關(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時(shí)才會重新執(zhí)行 if(isRun) { // 新增 // 保存執(zhí)行結(jié)果 value = effectFn() // 新增 // 執(zhí)行完畢后再次重置執(zhí)行開關(guān) isRun = false // 新增 } return value } } return obj }
但是上面的實(shí)現(xiàn)還有一個(gè)問題,就是好像isRun
執(zhí)行一次后好像永遠(yuǎn)都不會變成true
了,我們的本意是在數(shù)據(jù)發(fā)生變動(dòng)的時(shí)候需要再次觸發(fā)依賴函數(shù),也就是將isRun變?yōu)閠rue,實(shí)現(xiàn)這種效果,需要我們?yōu)?code>options再傳遞一個(gè)函數(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 }
接下來需要修改一下trigger
如果傳遞了scheduler
這個(gè)函數(shù),那么只執(zhí)行scheduler
這個(gè)函數(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() } }) }
那么在computed
中就可以實(shí)現(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時(shí)才會重新執(zhí)行 if(isRun) { // 新增 // 保存執(zhí)行結(jié)果 value = effectFn() // 新增 // 執(zhí)行完畢后再次重置執(zhí)行開關(guān) isRun = false // 新增 } return value } } return obj }
當(dāng)computed
傳入的依賴函數(shù)中的值發(fā)生改變時(shí),會觸發(fā)響應(yīng)式對象的trigger
函數(shù),而計(jì)算屬性創(chuàng)建響應(yīng)式對象時(shí)傳入了scheduler
,所以當(dāng)數(shù)據(jù)改變時(shí),只會執(zhí)行scheduler
函數(shù),在scheduler
函數(shù)內(nèi)我們將執(zhí)行開關(guān)重置為true
,再下次訪問數(shù)據(jù)觸發(fā)get
函數(shù)時(shí),就會重新執(zhí)行依賴函數(shù)。這也就實(shí)現(xiàn)了當(dāng)數(shù)據(jù)發(fā)生改變時(shí),會再次觸發(fā)依賴函數(shù)的功能了。
為了避免計(jì)算屬性被另外一個(gè)依賴函數(shù)調(diào)用而失去響應(yīng),我們還需要為計(jì)算屬性單獨(dú)進(jìn)行綁定響應(yīng)式的功能,形成一個(gè)effect
嵌套。
function computed(getter) { let value, isRun = true; const effectFn = effect(getter, { lazy: true, scheduler() { if(!isRun) { isRun = true // 當(dāng)計(jì)算屬性依賴的響應(yīng)式數(shù)據(jù)發(fā)生變化時(shí),手動(dòng)調(diào)用trigger函數(shù)觸發(fā)響應(yīng) trigger(obj, 'value') // 新增 } } }) const obj = { get value() { if(isRun) { value = effectFn() isRun = false } // 當(dāng)讀取value時(shí),手動(dòng)調(diào)用track函數(shù)進(jìn)行追蹤 track(obj, 'value') return value } } return obj }
五. watch
先來看一下watch
函數(shù)的用法,它的用法也非常簡單:
watch(obj, ()=>{ console.log(改變了) }) // 修改數(shù)據(jù),觸發(fā)watch函數(shù) obj.age++
watch
接受兩個(gè)參數(shù),第一個(gè)參數(shù)為綁定的響應(yīng)式數(shù)據(jù),第二個(gè)參數(shù)為依賴函數(shù),我們依然可以沿用之前的思路來進(jìn)行處理,利用effect
以及scheduler
來改變觸發(fā)執(zhí)行時(shí)機(jī)。
function watch(source, fn) { effect( // 遞歸讀取對象中的每一項(xiàng),變?yōu)轫憫?yīng)式數(shù)據(jù),綁定依賴函數(shù) ()=> bindData(source), { scheduler() { // 當(dāng)數(shù)據(jù)發(fā)生改變時(shí),調(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 }
watch
函數(shù)還有另外一種用法,就是除了接收對象,還可以接受一個(gè)getter
函數(shù),例如:
watch( ()=> obj.age, ()=> { console.log('改變了') } )
這種情況下只需要將用戶傳入的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ā)生改變時(shí),調(diào)用依賴函數(shù) fn() } } ) }
其實(shí)watch
函數(shù)還有一個(gè)很重要的功能:就是在用戶傳遞的依賴函數(shù)中可以獲取新值和舊值,但是我們目前還做不到這一點(diǎn)。實(shí)現(xiàn)這個(gè)功能我們可以配置前文中的lazy
屬性來實(shí)現(xiàn)。 來回顧一下lazy
屬性:設(shè)置了lazy
之后一開始不會執(zhí)行依賴函數(shù),手動(dòng)執(zhí)行時(shí)會返回執(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 // 新增 } } ) // 手動(dòng)調(diào)用依賴函數(shù),取得舊值 oldVal = effectFn() // 新增 }
此外,watch
函數(shù)還有一個(gè)功能,就是可以自定義執(zhí)行時(shí)機(jī),比如immediate
屬性,他會在創(chuàng)建時(shí)立即執(zhí)行一次:
watch(obj, ()=>{ console.log('改變了') }, { immediate: true })
我們可以把scheduler
封裝為一個(gè)函數(shù),以便在不同的時(shí)機(jī)去調(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時(shí),立即執(zhí)行一次依賴函數(shù) if(options.immediate) { // 新增 run() // 新增 } else { oldVal = effectFn() } }
watch
函數(shù)還支持其他的執(zhí)行調(diào)用時(shí)機(jī),這里只實(shí)現(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
因?yàn)橹皩?shí)現(xiàn)的攔截,無論對于什么類型的數(shù)據(jù)都是直接進(jìn)行返回的,如果實(shí)現(xiàn)深響應(yīng),那么首先應(yīng)該判斷是否為對象類型的值,如果是對象類型的值,應(yīng)當(dāng)遞歸調(diào)用reactive
方法進(jìn)行轉(zhuǎn)換。
// 接收第二個(gè)參數(shù),標(biāo)記為是否為淺響應(yīng) function createReactive(obj, isShallow = false) { return new Proxy(obj, { get(target, key, receiver) { // 訪問raw時(shí),返回原對象 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 }, // ...省略其他 })
將創(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) }
七. 淺只讀與深只讀
有時(shí)候我們并不需要對值進(jìn)行修改,也就是需要值為只讀的,這個(gè)操作也分為深只讀和淺只讀,首先需要在createReactive
函數(shù)中增加一個(gè)參數(shù)isReadOnly
,代表是否為只讀屬性。
// 淺只讀 function shallowReadOnly(obj) { return createReactive(obj, true, true) } // 深只讀 function readOnly(obj) { return createReactive(obj, false, true) }
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 }
如果為只讀屬性,那么也不需要為其建立響應(yīng)聯(lián)系 如果為只讀屬性,那么在進(jìn)行深層次遍歷的時(shí)候,需要調(diào)用readOnly
函數(shù)對值進(jìn)行包裝
function createReactive(obj, isShallow = false, isReadOnly = false) { return new Proxy(obj, { get(target, key, receiver) { // 訪問raw時(shí),返回原對象 if (key === 'raw') return target //只有在非只讀的時(shí)候才需要建立響應(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 }, }) }
八. 處理數(shù)組
數(shù)組的索引與length
如果操作數(shù)組時(shí),設(shè)置的索引值大于數(shù)組當(dāng)前的長度,那么要更新數(shù)組的length
屬性,所以當(dāng)通過索引設(shè)置元素值時(shí),可能會隱式的修改length
的屬性值,因此再j進(jìn)行觸發(fā)響應(yīng)時(shí),也應(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'
在判斷操作類型時(shí),新增對數(shù)組類型的判斷,如果代理目標(biāo)是數(shù)組,那么對于操作類型的判斷作出處理:
如果設(shè)置的索引值小于數(shù)組的長度,就視為SET
操作,因?yàn)樗粫淖償?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 },
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í)數(shù)組時(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() } }) }
還有一點(diǎn):其實(shí)修改數(shù)組的length
屬性也會隱式的影響數(shù)組元素:
const arr = reactive(['foo']) effect(()=>{ // 訪問數(shù)組的第0個(gè)元素 console.log(arrr[0]) // foo }) // 將數(shù)組的長度修改為0,導(dǎo)致第0個(gè)元素被刪除,因此應(yīng)該觸發(fā)響應(yīng) arr.length = 0
如上所示,在副作用函數(shù)內(nèi)部訪問了第0個(gè)元素,然后將數(shù)組的length
屬性修改為0,這回隱式的影響數(shù)組元素,及所有的元素都會被刪除,所以應(yīng)該觸發(fā)副作用函數(shù)重新執(zhí)行。
然而并非所有的對length
屬性值的修改都會影響數(shù)組中的已有元素,如果設(shè)置的length
屬性為100,這并不會影響第0個(gè)元素,當(dāng)修改屬性值時(shí),只有那些索引值大于等于新的length
屬性值的元素才需要觸發(fā)響應(yīng)。
調(diào)用trigger
函數(shù)時(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 }
判斷新的下標(biāo)值與需要操作的新的下標(biāo)值進(jìn)行判斷,因?yàn)閿?shù)組的key
為下標(biāo),所以副作用函數(shù)搜集器是以下標(biāo)作為key
值的,當(dāng)length
發(fā)生變動(dòng)時(shí),只需要將新值與每個(gè)下標(biāo)的key
判斷,大于等于新的length
值的需要重新執(zhí)行副作用函數(shù)。
如上圖所示,Map
為根據(jù)數(shù)組的key
,也就是id
組成的Map
結(jié)構(gòu),他們的每一個(gè)key
都對應(yīng)一個(gè)Set
,用于保存這個(gè)key
下面的所有的依賴函數(shù)。
當(dāng)length
屬性發(fā)生變動(dòng)時(shí),應(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),因?yàn)閿?shù)組中key為index if(key >= newValue) { effects.forEach(effectFn=>{ if (effectFn !== activeEffect) { effectsToRun.add(effectFn) } }) } }) } // ...省略 }
本文的實(shí)現(xiàn)數(shù)組這種數(shù)據(jù)結(jié)構(gòu)只考慮了針對長度發(fā)生變化的情況。
九. ref
由于Proxy的代理目標(biāo)是非原始值,所以沒有任何手段去攔截對原始值的操作:
let str = 'hi' // 無法攔截對值的修改 str = 'pino'
解決方法是:使用一個(gè)非原始值去包裹原始值:
function ref(val) { // 創(chuàng)建一個(gè)對象對原始值進(jìn)行包裹 const wrapper = { value: val } // 使用reactive函數(shù)將包裹對象編程響應(yīng)式數(shù)據(jù)并返回 return reactive(wrapper) }
如何判斷是用戶傳入的對象還是包裹對象呢?
const ref1 = ref(1) const ref2 = reactive({ value: 1 })
只需要在包裹對象內(nèi)部定義一個(gè)不可枚舉且不可寫的屬性:
function ref(val) { // 創(chuàng)建一個(gè)對象對原始值進(jìn)行包裹 const wrapper = { value: val } // 定義一個(gè)屬性值__v_isRef,值為true,代表是包裹對象 Object.defineProperty(wrapper, '_isRef', { value: true }) // 使用reactive函數(shù)將包裹對象編程響應(yīng)式數(shù)據(jù)并返回 return reactive(wrapper) }
十. 響應(yīng)丟失問題與toRefs
在使用…解構(gòu)賦值時(shí)會導(dǎo)致響應(yīng)式丟失:
const obj = reactive({ foo: 1, bar: 2 }) // 將響應(yīng)式數(shù)據(jù)展開到一個(gè)新的對象newObj const newObj = { ...obj } // 此時(shí)相當(dāng)于: const newObj = { foo: 1, bar: 2 } effect(()=>{ //在副作用函數(shù)中通過新對象newObj讀取foo屬性值 console.log(newObj.foo) }) // obj,foo并不會觸發(fā)響應(yīng) obj.foo = 100
首先創(chuàng)建一個(gè)響應(yīng)式對象obj,然后使用展開運(yùn)算符得到一個(gè)新對象newObj
,他是一個(gè)普通對象,不具有響應(yīng)式的能力,所以修改obj.foo
的值不會觸發(fā)副作用函數(shù)重新更新。
解決方法:
const newObj = { foo: { // 用于返回其原始的響應(yīng)式對象 get value() { return obj.foo } }, bar: { get value() { return obj.bar } } }
將單個(gè)值包裝為一個(gè)對象,相當(dāng)于訪問該屬性的時(shí)候會得到該屬性的getter
,在getter
中返回原始的響應(yīng)式對象。
相當(dāng)于解構(gòu)訪問newObj.foo
=== obj.foo
。
{ get value() { return obj.foo } }
toRefs
function toRefs(obj) { let res = {} // 處理整個(gè)對象時(shí),將屬性依次進(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 }
使用toRefs
處理整個(gè)對象,在toRefs
這個(gè)函數(shù)中循環(huán)處理了對象所包含的所有屬性。
const newObj = { ...toRefs(obj) }
當(dāng)設(shè)置value
屬性值的時(shí)候,最終設(shè)置的是響應(yīng)式數(shù)據(jù)的同名屬性值。
一個(gè)基本的vue3
響應(yīng)式就完成了,但是本文所實(shí)現(xiàn)的依然是閹割版本,有很多情況都沒有進(jìn)行考慮,還有好多功能沒有實(shí)現(xiàn),比如:攔截 Map
,Set
,數(shù)組的其他問題,對象的其他問題,其他api的實(shí)現(xiàn),但是上面的實(shí)現(xiàn)已經(jīng)足夠讓你理解vue3響應(yīng)式原理實(shí)現(xiàn)的核心了,這里還有很多其他的資料需要推薦,比如阮一峰老師的es6教程,對于vue3底層原理的實(shí)現(xiàn),許多知識依然是需要回顧和復(fù)習(xí),查看原始底層的實(shí)現(xiàn),再比如霍春陽老師的《vue.js的設(shè)計(jì)與實(shí)現(xiàn)》這本書,這本書目前我也只看完了一半,但是截止到目前我認(rèn)為這本書對于學(xué)習(xí)vue3
的原理是非常深入淺出,鞭辟入里的,本文的許多例子也是借鑒了這本書。
最后當(dāng)然是需要取讀一讀源碼,不過在讀源碼之前能夠先了解一下實(shí)現(xiàn)的核心原理,再去看源碼是事半功倍的。希望大家都能早日學(xué)透源碼,面試的時(shí)候能夠?qū)Υ鹑缌?,工作中遇到的問題也能從原理層面去理解和更好地解決!
目前我也在實(shí)現(xiàn)一個(gè)mini-vue
,截止到目前只實(shí)現(xiàn)了響應(yīng)式部分,而且與本文的實(shí)現(xiàn)方式有所不同,后續(xù)還會繼續(xù)實(shí)現(xiàn)編譯和虛擬DOM部分,歡迎star!
k-vue:
https://github.com/konvyi/k-vue
如果想學(xué)習(xí)《vue.js的設(shè)計(jì)與實(shí)現(xiàn)》這本書這本書,那么請關(guān)注下面這個(gè)鏈接作為參考,里面包含了根據(jù)具體的問題的功能進(jìn)行拆分實(shí)現(xiàn),同樣也只實(shí)現(xiàn)了響應(yīng)式的部分!
vue3-analysis:
https://github.com/konvyi/vue3-analysis
(學(xué)習(xí)視頻分享:web前端開發(fā)、編程基礎(chǔ)視頻)
聲明:本文轉(zhuǎn)載于:掘金社區(qū),如有侵犯,請聯(lián)系admin@php.cn刪除
推薦:PHP從基礎(chǔ)到實(shí)戰(zhàn)教程視頻
- 上一篇:2022七夕情人節(jié)表白特效代碼+網(wǎng)站素材【免費(fèi)下載】
- 下一篇:沒有了