最近在研究各個框架的源碼,從源碼角度去理解 vue3 的 reactive 和 ref API,記錄下研究的成果
reactive
首先,reactive() 的參數必須是一個對象,返回值是一個 Proxy 對象,具有響應性。如果參數不是對象類型,會提示:value cannot be made reactive。
多次對同一個對象使用 reactive 進行代理,返回的是相同的代理對象,也就是說使用的是緩存的值。而且,取值時直接讀取屬性就行,不需要加 .value 。
例子:
import { reactive } from 'vue'
const state = reactive({ count: 0 })
console.log(state.count) // 0const name = reactive('hh')
console.log('name', name) // warn: value cannot be made reactive: hhconst raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) // false
// calling reactive() on the same object returns the same proxy
console.log(reactive(raw) === proxy) // true
// calling reactive() on a proxy returns itself
console.log(reactive(proxy) === proxy) // true
接下來說下 reactive 的局限性。
首先,參數只支持 object 類型 (比如 objects, arrays, Map, Set),不支持基礎數據類型,比如string, number 或boolean;
其次,對變量重新賦值會丟失響應性,比如:
let state = reactive({ count: 0 })
// the above reference ({ count: 0 }) is no longer being tracked
// (reactivity connection is lost!)
state = reactive({ count: 1 })
而且,解構賦值容易丟失響應性:
const state = reactive({ count: 0 })// count is disconnected from state.count when destructured.
let { count } = state
// does not affect original state
count++
這種情況下,我們可以使用 toRefs 函數來將響應式對象轉換為 ref 對象
import { toRefs } from 'vue';const state = reactive({ count: 0 });
let { count } = toRefs(state);
count++; // count 現在是 1
ref
再來看下 ref() 。reactive 和 ref 都是聲明響應式變量的寫法,但是,ref 的參數既可以是基本數據類型的值,也可以是對象,很自由!這就是為什么我們在開發時更推薦使用 vue3 的 ref 的原因了。
而且,ref 聲明的變量在取值時必須加上 .value,而在 template 調用時中不加。
例子:
再來看下 ref() 。reactive 和 ref 都是聲明響應式變量的寫法,但是,ref 的參數既可以是基本數據類型的值,也可以是對象,很自由!這就是為什么我們在開發時更推薦使用 vue3 的 ref 的原因了。
而且,ref 聲明的變量在取值時必須加上 .value,而在 template 調用時中不加。
例子:
const {ref, effect} = Vueconst name = ref('張三')
console.log('name', name.value) // name 張三const state = ref({ count: 0 })
console.log('state', state.value.count) // state 0
ref 源碼
深入源碼看下為什么。
ref() 中調用的是 createRef(value, false),在這個函數中,首先判斷屬性 __v_isRef 是否為 true,為 true 說明是 Ref 類型的值,直接返回;否則,返回的是 RefImpl 類的實例。
類的 get 和 set
再來看 RefImpl 類,重點是類中定義了 get 函數和 set 函數。當我們對類實例的 value 屬性取值和賦值時,就會觸發這兩個函數。
// ref.tsexport function ref(value?: unknown) {return createRef(value, false)
}function createRef(rawValue: unknown, shallow: boolean) {// 判斷屬性 __v_isRef 是否為 true,為 true 說明是 Ref 類型的值,直接返回if (isRef(rawValue)) {return rawValue}return new RefImpl(rawValue, shallow)
}export function isRef(r: any): r is Ref {return !!(r && r.__v_isRef === true)
}class RefImpl<T> {private _value: Tprivate _rawValue: T// 依賴項public dep?: Dep = undefined// 屬性 __v_isRef 設置為 truepublic readonly __v_isRef = trueconstructor(value: T, public readonly __v_isShallow: boolean) {this._rawValue = __v_isShallow ? value : toRaw(value)this._value = __v_isShallow ? value : toReactive(value)}get value() {// 依賴收集trackRefValue(this)// 返回值return this._value}set value(newVal) {const useDirectValue =this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)newVal = useDirectValue ? newVal : toRaw(newVal)if (hasChanged(newVal, this._rawValue)) {this._rawValue = newValthis._value = useDirectValue ? newVal : toReactive(newVal)triggerRefValue(this, newVal)}}
}
舉個例子理解下類中的 get 和 set 函數:
class RefImpl {// ref實例的getter行為get value () {console.log('get');return '111'}// ref實例的setter行為set value (val) {console.log('set');}
}const ref = new RefImpl()ref.value = '123'
ref.value
這里定義了 RefImpl 類,當我們對 ref.value 賦值時,會打印 set;當我們調用 ref.value 時,會打印 get。因此,我們不難理解為什么 Vue3 的 ref() 要加上 .value 了,因為也是使用了類中的 getter 和 setter 的寫法。
此外,ref() 最終的返回值是 this._value,我們再來看下這部分的代碼。這里是判斷屬性 __v_isShallow 是否為 true,為true 則直接返回,否則經過 toReactive() 處理下再返回。
this._value = __v_isShallow ? value : toReactive(value)
toReactive()
看下這個函數發生了什么。可以看到,如果參數是對象類型,則使用 reactive() 處理一下并返回;否則直接返回這個參數。
而 reactive() 中,我們是返回一個對象的 Proxy 對象,這個 Proxy 對象具有響應性,可以監聽到我們對對象屬性的讀取和修改。值得一提的是,這里的 reactive() 正是 上面說到的聲明響應性變量的 reactive() !也就是說,ref 的底層也用到了 reactive() ,二者是相通的,只不過 ref 多包裝了一層,支持了基本數據類型的值。
// reactive.ts/*** Returns a reactive proxy of the given value (if possible).** If the given value is not an object, the original value itself is returned.** @param value - The value for which a reactive proxy shall be created.*/
export const toReactive = <T extends unknown>(value: T): T =>isObject(value) ? reactive(value) : value/*** Returns a reactive proxy of the object.** The reactive conversion is "deep": it affects all nested properties. A* reactive object also deeply unwraps any properties that are refs while* maintaining reactivity.** @example* ```js* const obj = reactive({ count: 0 })* ```** @param target - The source object.* @see {@link https://vuejs.org/api/reactivity-core.html#reactive}*/
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {// if trying to observe a readonly proxy, return the readonly version.if (isReadonly(target)) {return target}return createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers,reactiveMap)
}
createReactiveObject()
看下響應性是如何實現的。
首先,在 createReactiveObject() 函數中,如果傳參 target 是非對象類型的,會提示并直接返回,我們之前的例子中也觀察到這種現象了;
其次,判斷 target 是否是 Proxy 或者已經存在哈希表 proxyMap 中,如果是直接返回;
最后,如果傳參只是一個普通的對象,我們需要使用 new Proxy() 將其轉化為一個 Proxy 對象,我們知道在 Vue3 中響應性的實現正是通過 Proxy 去實現的。生成 Proxy 對象后,存入 proxyMap 中,并返回該 Proxy 對象即可。
function createReactiveObject(target: Target,isReadonly: boolean,baseHandlers: ProxyHandler<any>,collectionHandlers: ProxyHandler<any>,proxyMap: WeakMap<Target, any>
) {if (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)}return target}// target is already a Proxy, return it.// exception: calling readonly() on a reactive objectif (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}// target already has corresponding Proxyconst existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}// only specific value types can be observed.const targetType = getTargetType(target)if (targetType === TargetType.INVALID) {return target}const proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)proxyMap.set(target, proxy)return proxy
}
小結:
createReactiveObject 函數,即 reactive 函數,最終是將傳參的對象轉化為一個 Proxy 對象并返回,而 Vue3 中響應性的實現正是通過 Proxy 去實現的。