閱讀本文須要有必定的TypeScript基礎,要求不高,看過一遍TS的文檔便可。html
咱們閱讀源碼的緣由是什麼?無非是1:學習;2:更好的使用這個庫。若是隻是想大體的瞭解下原理,倒沒必要花時間閱讀源碼,幾句話,幾張圖就能搞清楚,網上搜搜應該就有不少。所以,閱讀源碼的過程必定是要對不明白的地方深刻了解,確定是很費時間的。vue
在這過程當中,有些知識點,跟庫自己可能沒什麼關係,但若是不懂,又難繼續理解。對於這些知識點,我會盡可能少的解釋,但會貼上儘可能完善的文檔,方便不瞭解的同窗先閱讀學習。react
鑑於篇幅太長,信息量較大,我會將文章拆開,邊寫邊發,有興趣的同窗能夠連載閱讀,寫完之後再彙總一篇,方便時間充沛的同窗一股腦看。git
在上篇文章中說道,ref是最影響源碼閱讀的文件。但若是不先搞明白它,看其餘的只會更暈。我先幫你們理清ref的邏輯跟概念。github
因爲如今(2019/10/9)vue@3還未正式發版,你們還不熟悉其相關的用法。上篇文章雖然介紹了很多,但其實仍是有很多疑問。在閱讀本篇文章以前,若是有時間,建議先閱讀Vue官方對Composition API的介紹:typescript
讀完關於Composition API的介紹,會對了解本庫有更多認識,便於更好的理解源碼。api
ref
跟reactive
是整個源碼中的核心,經過這兩個方法建立了響應式數據。要想徹底吃透reactivity
,必須先吃透這兩個。數組
ref最重要的做用,實際上是提供了一套Ref
類型,咱們先來看,它究竟是個怎麼樣的數據類型。(爲了更好的作解釋,我會調整源碼中的接口、類型、函數等聲明順序,並會增長一些註釋方便閱讀)數據結構
// 生成一個惟一key,開發環境下增長描述符 'refSymbol'
export const refSymbol = Symbol(__DEV__ ? 'refSymbol' : undefined)
// 聲明Ref接口
export interface Ref<T = any> {
// 用此惟一key,來作Ref接口的一個描述符,讓isRef函數作類型判斷
[refSymbol]: true
// value值,存放真正的數據的地方。關於UnwrapNestedRefs這個類型,我後續單獨解釋
value: UnwrapNestedRefs<T>
}
// 判斷是不是Ref數據的方法
// 對於is關鍵詞,若不熟悉,見:http://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates
export function isRef(v: any): v is Ref {
return v ? v[refSymbol] === true : false
}
// 見下文解釋
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
複製代碼
要想了解UnwrapNestedRefs
與UnwrapRef
,必須先要了解ts中的infer
。若是以前不瞭解,請先閱讀相關文檔。看完文檔,再建議去google一些案例看看加深下印象。app
如今咱們假設你瞭解了infer
概念,也瞭解了它的平常用法。再來看源碼:
// 不該該繼續遞歸的引用數據類型
type BailTypes =
| Function
| Map<any, any>
| Set<any>
| WeakMap<any, any>
| WeakSet<any>
// 遞歸地獲取嵌套數據的類型
// Recursively unwraps nested value bindings.
export type UnwrapRef<T> = {
// 若是是ref類型,繼續解套
ref: T extends Ref<infer V> ? UnwrapRef<V> : T
// 若是是數組,循環解套
array: T extends Array<infer V> ? Array<UnwrapRef<V>> : T
// 若是是對象,遍歷解套
object: { [K in keyof T]: UnwrapRef<T[K]> }
// 不然,中止解套
stop: T
}[T extends Ref
? 'ref'
: T extends Array<any>
? 'array'
: T extends BailTypes
? 'stop' // bail out on types that shouldn't be unwrapped
: T extends object ? 'object' : 'stop']
// 聲明類型別名:UnwrapNestedRefs
// 它是這樣的類型:若是該類型已經繼承於Ref,則不須要解套,不然多是嵌套的ref,走遞歸解套
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
複製代碼
若是仍是懵,建議後續再去看看infer的相關介紹。在這咱們直接拋結果:
Ref
是這樣的一種數據結構:它有個key爲Symbol
的屬性作類型標識,有個屬性value
用來存儲數據。這個數據能夠是任意的類型,惟獨不能是被嵌套了Ref
類型的類型。 具體來講就是不能是這樣 Array<Ref>
或者這樣 { [key]: Ref }
。但很奇怪的是,這樣Ref<Ref>
又是能夠的。具體爲何也不知道,因此我勇敢地提了個PR...
(果真Ref<Ref>
是不夠完美的,2019.10.10晚,我這PR被合併了。你們遇到疑問時,也能夠勇敢的提PR,說不定就被合了....)
另外,Map、Set、WeakMap、WeakSet也是不支持解套的。說明Ref
數據的value也有多是Map<Ref>
這樣的數據類型。
說回Ref,從上篇文章中,咱們已經瞭解到,Ref
類型的數據,是一種響應式的數據。而後咱們看其具體實現:
// 從@vue/shared中引入,判斷一個數據是否爲對象
// Record<any, any>表明了任意類型key,任意類型value的類型
// 爲何不是 val is object 呢?能夠看下這個回答:https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: any): val is Record<any, any> =>
val !== null && typeof val === 'object'
// 若是傳遞的值是個對象(包含數組/Map/Set/WeakMap/WeakSet),則使用reactive執行,不然返回原數據
// 從上篇文章知道,這個reactive就是將咱們的數據轉成響應式數據
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)
export function ref<T>(raw: T): Ref<T> {
// 轉化數據
raw = convert(raw)
const v = {
[refSymbol]: true,
get value() {
// track的代碼在effect中,暫時不看,能猜到此處就是監聽函數收集依賴的方法。
track(v, OperationTypes.GET, '')
// 返回剛剛被轉化後的數據
return raw
},
set value(newVal) {
// 將設置的值,轉化爲響應式數據,賦值給raw
raw = convert(newVal)
// trigger也暫時不看,能猜到此處就是觸發監聽函數執行的方法
trigger(v, OperationTypes.SET, '')
}
}
return v as Ref<T>
}
複製代碼
其實最難理解的就在於這個ref
函數。咱們看到,這裏也定義了get/set,卻沒有任何Proxy
相關的操做。在以前的信息中咱們知道reactive
能構建出響應式數據,但要求傳參必須是對象。但ref
的入參是對象時,一樣也須要reactive
作轉化。那ref
這個函數的目的究竟是什麼呢?爲何須要有它?
在文章開頭,我貼了這份官方介紹Ref vs Reactive,這其中其實已經說的很明白。
However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:
對於基本數據類型,函數傳遞或者對象解構時,會丟失原始數據的引用,換言之,咱們無法讓基本數據類型,或者解構後的變量(若是它的值也是基本數據類型的話),成爲響應式的數據。
// 咱們是永遠沒辦法讓`a`或`x`這樣的基本數據成爲響應式的數據的,Proxy也沒法劫持基本數據。
const a = 1;
const { x: 1 } = { x: 1 }
複製代碼
可是有時候,咱們確實就是想一個數字、一個字符串是響應式的,或者就是想利用解構的寫法。那怎麼辦呢?只能經過建立一個對象,也便是源碼中的Ref
數據,而後將原始數據保存在Ref
的屬性value
當中,再將它的引用返回給使用者。既然是咱們本身創造出來的對象,也就不必使用Proxy
再作代理了,直接劫持這個value
的get/set便可,這就是ref
函數與Ref
類型的由來。
不過單靠ref
還無法解決對象解構的問題,它只是將基本數據保持在一個對象的value
中,以實現數據響應式。對於對象的解構還須要另一個函數:toRefs
。
export function toRefs<T extends object>( object: T ): { [K in keyof T]: Ref<T[K]> } {
const ret: any = {}
// 遍歷對象的全部key,將其值轉化爲Ref數據
for (const key in object) {
ret[key] = toProxyRef(object, key)
}
return ret
}
function toProxyRef<T extends object, K extends keyof T>( object: T, key: K ): Ref<T[K]> {
const v = {
[refSymbol]: true,
get value() {
// 注意,這裏沒用到track
return object[key]
},
set value(newVal) {
// 注意,這裏沒用到trigger
object[key] = newVal
}
}
return v as Ref<T[K]>
}
複製代碼
經過遍歷對象,將每一個屬性值都轉成Ref
數據,這樣解構出來的仍是Ref
數據,天然就保持了響應式數據的引用。可是源碼中有一點要注意,toRefs
函數中引用的是toProxyRef
而不是ref
,它並不會在get/set中注入track
跟trigger
,也就是說,**向toRefs
傳入一個正常的對象,是不會返回一個響應式的數據的。**必需要傳遞一個已經被reactive
執行返回的對象纔能有響應式的效果。感受這點能夠優化,暫時也不知道小右這樣作的緣由是什麼。因爲這裏會牽扯到track
跟trigger
,而這兩個在我寫本文時還沒研究,就沒膽子提PR了。
到這,咱們就把ref
的源碼給看完了。
下一章節咱們開始看reactive
,它是核心,從它開始,內部的各個api
開始真正的串連。
本文做者:螞蟻保險-體驗技術組-阿相
掘金地址:相學長