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