我們閱讀源碼的原因是什麼?無非是1:學習;2:更好的使用這個庫。如果只是想大致的了解下原理,倒不必花時間閱讀源碼,幾句話,幾張圖就能搞清楚,網上搜搜應該就有很多。因此,閱讀源碼的過程一定是要對不明白的地方深入了解,肯定是很費時間的。
在這過程中,有些知識點,跟庫本身可能沒什麼關係,但如果不懂,又難繼續理解。對於這些知識點,我會儘量少的解釋,但會貼上儘量完善的文檔,方便不了解的同學先閱讀學習。
鑑於篇幅太長,信息量較大,我會將文章拆開,邊寫邊發,有興趣的同學可以連載閱讀,寫完以後再匯總一篇,方便時間充沛的同學一股腦看。
前言在上篇文章中說道,ref是最影響源碼閱讀的文件。但如果不先搞明白它,看其他的只會更暈。我先幫大家理清ref的邏輯跟概念。
由於現在(2019/10/9)vue@3還未正式發版,大家還不熟悉其相關的用法。上篇文章雖然介紹了不少,但其實還是有不少疑問。在閱讀本篇文章之前,如果有時間,建議先閱讀Vue官方對Composition API的介紹: 1. Vue Composition API 2. Ref Vs Reactive
讀完關於Composition API的介紹,會對了解本庫有更多認識,便於更好的理解源碼。
ref跟reactive是整個源碼中的核心,通過這兩個方法創建了響應式數據。要想完全吃透reactivity,必須先吃透這兩個。
Refref最重要的作用,其實是提供了一套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一些案例看看加深下印象。
現在我們假設你了解了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的源碼給看完了。