Vue3響應式對象Reactive和Ref的用法解讀
一、內(nèi)容簡介
本篇文章著重結合源碼版本V3.2.20介紹Reactive和Ref。前置技能需要了解Proxy對象的工作機制,以下貼出的源碼均在關鍵位置備注了詳細注釋。
備注:本篇幅只講到收集依賴和觸發(fā)依賴更新的時機,并未講到如何收集依賴和如何觸發(fā)依賴。響應式原理快捷通道。
二、Reactive
1. 關鍵源碼
/*源碼位置:/packages/reactivity/src/reactive.ts*/ /** ?* 創(chuàng)建響應式代理對象 ?* @param target 被代理對象 ?* @param isReadonly 是否只讀 ?* @param baseHandlers 普通對象的攔截操作 ?* @param collectionHandlers 集合對象的攔截操作 ?* @param proxyMap 代理Map ?* @returns? ?*/ function createReactiveObject( ? target: Target, ? isReadonly: boolean, ? baseHandlers: ProxyHandler<any>, ? collectionHandlers: ProxyHandler<any>, ? proxyMap: WeakMap<Target, any> ) { ? //如果不是對象,則警告,Proxy代理只支持對象 ? if (!isObject(target)) { ? ? if (__DEV__) { ? ? ? console.warn(`value cannot be made reactive: ${String(target)}`) ? ? } ? ? return target ? } ? //如果被代理對象已經(jīng)是一個proxy對象且是響應式的并且此次創(chuàng)建的新代理對象不是只讀的,則直接返回被代理對象 ? //這兒存在一種情況需要重新創(chuàng)建,即被代理對象已經(jīng)是一個代理對象了,且可讀可寫。但新創(chuàng)建的代理對象是只讀的 ? //那么,本次生成的那個代理對象最終是只讀的。響應式必須可讀可寫,只讀的代理對象是非響應式的。 ? if ( ? ? target[ReactiveFlags.RAW] && ? ? !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ? ) { ? ? return target ? } ? //從map中找,如果對象已經(jīng)被代理過,則直接從map中返回,否則生成代理 ? const existingProxy = proxyMap.get(target) ? if (existingProxy) { ? ? return existingProxy ? } ? // 獲取代理類型,即采用集合類型的代理還是普通對象類型的代理 ? const targetType = getTargetType(target) ? if (targetType === TargetType.INVALID) { ? ? return target ? } ? // 生成代理對象并存入map中 ? const proxy = new Proxy( ? ? target, ? ? targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ? ) ? proxyMap.set(target, proxy) ? return proxy }
2. 源碼流程分析
Vue中創(chuàng)建響應式代理對象都是通過createReactiveObject方法創(chuàng)建。這個方法里面的主要邏輯很簡單,就是生成一個目標對象的代理對象,代理對象最為核心的操作攔截則由外部根據(jù)是否只讀和是否淺響應傳入,然后將這個代理對象存起來以備下次快捷獲取。
三、代理攔截操作
1. 數(shù)組操作
(1).關鍵源碼
//源碼位置: /packages/reactivity/src/baseHandlers.ts function createArrayInstrumentations() { ? const instrumentations: Record<string, Function> = {} ? // instrument identity-sensitive Array methods to account for possible reactive ? // values ? ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => { ? ? instrumentations[key] = function (this: unknown[], ...args: unknown[]) { ? ? ? //獲取原始數(shù)組 ? ? ? const arr = toRaw(this) as any ? ? ? for (let i = 0, l = this.length; i < l; i++) { ? ? ? ? //收集依賴 鍵值為索引 i ? ? ? ? track(arr, TrackOpTypes.GET, i + '') ? ? ? } ? ? ? // 調(diào)用數(shù)組的原始方法 ? ? ? const res = arr[key](...args) ? ? ? if (res === -1 || res === false) { ? ? ? ? // 如果不存在,則將參數(shù)參數(shù)轉(zhuǎn)換為原始數(shù)據(jù)在試一次(這兒可能是防止傳入的是代理對象導致獲取失敗) ? ? ? ? return arr[key](...args.map(toRaw)) ? ? ? } else { ? ? ? ? return res ? ? ? } ? ? } ? }) ? // instrument length-altering mutation methods to avoid length being tracked ? // which leads to infinite loops in some cases (#2137) ? ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { ? ? instrumentations[key] = function (this: unknown[], ...args: unknown[]) { ? ? ? //由于上面的方法會改變數(shù)組長度,因此暫停收集依賴,不然會導致無限遞歸 ? ? ? pauseTracking() ? ? ? //調(diào)用原始方法 ? ? ? const res = (toRaw(this) as any)[key].apply(this, args) ? ? ? //復原依賴收集 ? ? ? resetTracking() ? ? ? return res ? ? } ? }) ? return instrumentations }
(2).源碼流程分析
上述源碼其實就是重寫了對于數(shù)組方法的操作,在通過數(shù)組的代理對象訪問以上數(shù)組方法時,就會執(zhí)行重寫后的數(shù)組方法。
內(nèi)部邏輯很簡單,對于改變了數(shù)組長度的方法,先暫停依賴收集,調(diào)用原始數(shù)組方法,然后復原依賴收集。
對于判斷元素是否存在的數(shù)組方法,執(zhí)行依賴收集并調(diào)用數(shù)組原始方法。
總結來說最終都是調(diào)用了數(shù)組的原始方法,只不過在調(diào)用前后添加了關于依賴收集相關的行為。
2.Get操作
(1).關鍵源碼
//源碼位置: /packages/reactivity/src/baseHandlers.ts /** ?* 創(chuàng)建并且返回一個Get方法 ?* @param isReadonly 是否只讀 ?* @param shallow 是否淺響應 ?* @returns? ?*/ function createGetter(isReadonly = false, shallow = false) { ? return function get(target: Target, key: string | symbol, receiver: object) { ? ? //這兒不重要,其實就是通過代理對象訪問這幾個特殊屬性時,返回相應的值,和響應式無關 ? ? if (key === ReactiveFlags.IS_REACTIVE) { ? ? ? return !isReadonly ? ? } else if (key === ReactiveFlags.IS_READONLY) { ? ? ? return isReadonly ? ? } else if ( ? ? ? key === ReactiveFlags.RAW && ? ? ? receiver === ? ? ? ? (isReadonly ? ? ? ? ? ? shallow ? ? ? ? ? ? ? shallowReadonlyMap ? ? ? ? ? ? : readonlyMap ? ? ? ? ? : shallow ? ? ? ? ? ? shallowReactiveMap ? ? ? ? ? : reactiveMap ? ? ? ? ).get(target) ? ? ) { ? ? ? return target ? ? } ?? ? ? const targetIsArray = isArray(target) ? ? //如果是調(diào)用的數(shù)組方法,則調(diào)用重寫后的數(shù)組方法,前提不是只讀的 ? ? if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { ? ? ? return Reflect.get(arrayInstrumentations, key, receiver) ? ? } ? ? //調(diào)用原始行為獲取值 ? ? const res = Reflect.get(target, key, receiver) ? ? //訪問Symbol對象上的屬性和__proto__,__v_isRef,__isVue這3個屬性,直接返回結果值 ? ? if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { ? ? ? return res ? ? } ? ? if (!isReadonly) { ? ? ? //不是只讀,則收集依賴 ? ? ? track(target, TrackOpTypes.GET, key) ? ? } ? ? if (shallow) { ? ? ? //如果對象是淺響應的 則返回結果 ? ? ? return res ? ? } ? ? if (isRef(res)) { ? ? ? //如果值是Ref對象且是通過數(shù)組代理對象的下標訪問的,則不做解包裝操作,否則返回解包裝后的值 ? ? ? // ref unwrapping - does not apply for Array + integer key. ? ? ? const shouldUnwrap = !targetIsArray || !isIntegerKey(key) ? ? ? return shouldUnwrap ? res.value : res ? ? } ? ? if (isObject(res)) { ? ? ? //走到這兒需要滿足非淺響應。如果結果是一個對象,則將改對象轉(zhuǎn)換為只讀代理對象或者響應式代理對象返回 ? ? ? //e.g.? ? ? ? // test:{ ? ? ? // ? a:{ ? ? ? // ? ? c:10 ? ? ? // ? } ? ? ? // } ? ? ? //以上測試對象當訪問屬性a時,此時res是一個普通對象,如果不轉(zhuǎn)換為代理對象,則對a.c的操作不會被攔截處理,導致無法響應式處理 ? ? ? // Convert returned value into a proxy as well. we do the isObject check ? ? ? // here to avoid invalid value warning. Also need to lazy access readonly ? ? ? // and reactive here to avoid circular dependency. ? ? ? return isReadonly ? readonly(res) : reactive(res) ? ? } ? ?? ? ? return res ? } }
(2).源碼流程分析
上述Get方法是在通過代理對象獲取某一個值時觸發(fā)的。流程很簡單,就是對幾個特殊屬性做了特殊返回。
如果是數(shù)組方法,則調(diào)用重寫后的數(shù)組方法,不是則調(diào)用原始行為獲取值。
如果不是只讀,則收集依賴,對返回結果進行判斷特殊處理。其中最關鍵的地方在于收集依賴和將獲取到的嵌套對象轉(zhuǎn)換為響應式對象。
3. Set操作
(1).關鍵源碼
//源碼位置: /packages/reactivity/src/baseHandlers.ts /** ?* 創(chuàng)建并返回一個Set方法 ?* @param shallow 是否淺響應 ?* @returns? ?*/ function createSetter(shallow = false) { ? return function set( ? ? target: object, ? ? key: string | symbol, ? ? value: unknown, ? ? receiver: object ? ): boolean { ? ? //獲取改變之前的值 ? ? let oldValue = (target as any)[key] ? ? if (!shallow) { ? ? ? value = toRaw(value) ? ? ? oldValue = toRaw(oldValue) ? ? ? //對Ref類型值的特殊處理 ? ? ? //比較2個值,如果舊值是Ref對象,新值不是,則直接變Ref對象的value屬性 ? ? ? if (!isArray(target) && isRef(oldValue) && !isRef(value)) { ? ? ? ? //這兒看似沒有觸發(fā)依賴更新,其實Ref對象的value進行賦值會觸發(fā)Ref對象的寫操作,在那個操作里面會觸發(fā)依賴更新 ? ? ? ? oldValue.value = value ? ? ? ? return true ? ? ? } ? ? } else { ? ? ? // in shallow mode, objects are set as-is regardless of reactive or not ? ? } ? ? const hadKey = ? ? ? isArray(target) && isIntegerKey(key) ? ? ? ? ? Number(key) < target.length ? ? ? ? : hasOwn(target, key) ? ? const result = Reflect.set(target, key, value, receiver) ? ? // don't trigger if target is something up in the prototype chain of original ? ? // 這個判斷其實是處理一個代理對象的原型也是代理對象的情況,以下是測試代碼 ? ? // let hiddenValue: any ? ? // const obj = reactive<{ prop?: number }>({}) ? ? // const parent = reactive({ ? ? // ? set prop(value) { ? ? // ? ? hiddenValue = value ? ? // ? }, ? ? // ? get prop() { ? ? // ? ? return hiddenValue ? ? // ? } ? ? // }) ? ? // Object.setPrototypeOf(obj, parent) ? ? // obj.prop = 4 ? ? // 當存在上述情形,第一次設置值時,由于子代理沒有prop屬性方法,會觸發(fā)父代理的set方法。父代理的這個判斷此時是false,算是一個優(yōu)化,避免2個觸發(fā)更新 ? ? if (target === toRaw(receiver)) { ? ? ? if (!hadKey) { ? ? ? ? //觸發(fā)add類型依賴更新 ? ? ? ? trigger(target, TriggerOpTypes.ADD, key, value) ? ? ? } else if (hasChanged(value, oldValue)) { ? ? ? ? //觸發(fā)set類型依賴更新 ? ? ? ? trigger(target, TriggerOpTypes.SET, key, value, oldValue) ? ? ? } ? ? } ? ? return result ? } }
(2).源碼流程分析
當設置時,首先對舊值是Ref類型對象做了個特殊處理,如果滿足條件,則走Ref對象的set方法邏輯觸發(fā)依賴更新。
否則根據(jù)是否存在key值,判斷是新增屬性,還是修改屬性,觸發(fā)不同類型的依賴更新。
之所以要區(qū)分依賴類型,是因為某些屬性會連帶別的屬性更改,比如數(shù)組直接設置下標,會導致length的更改,這個時候需要收集length為鍵值的依賴,以便連帶更新依賴的length屬性的地方。
4. 其余行為攔截操作
(1).關鍵源碼
//源碼位置: /packages/reactivity/src/baseHandlers.ts /** ?* delete操作符時觸發(fā) ?* @param target 目標對象 ?* @param key 鍵值 ?* @returns? ?*/ function deleteProperty(target: object, key: string | symbol): boolean { ? const hadKey = hasOwn(target, key) ? const oldValue = (target as any)[key] ? const result = Reflect.deleteProperty(target, key) ? if (result && hadKey) { ? ? //觸發(fā)依賴更新 ? ? trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) ? } ? return result } /** ?* in 操作符時觸發(fā) ?* @param target 目標對象 ?* @param key 鍵值 ?* @returns? ?*/ function has(target: object, key: string | symbol): boolean { ? const result = Reflect.has(target, key) ? if (!isSymbol(key) || !builtInSymbols.has(key)) { ? ?? ?//收集依賴 ? ? track(target, TrackOpTypes.HAS, key) ? } ? return result } /** ?* Object.keys()等類似方法時調(diào)用 ?* @param target 目標對象 ?* @returns? ?*/ function ownKeys(target: object): (string | symbol)[] { ? //收集依賴 ? track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY) ? return Reflect.ownKeys(target) }
(2).源碼流程分析
上述源碼其實就是在對一些特殊操作符或者特定API時的特殊處理,本質(zhì)還是收集依賴和觸發(fā)依賴更新,沒什么好講的。
四、Ref對象
1. 思考一個問題
為什么存在了Reactive代理對象后,已經(jīng)可以進行依賴收集和依賴更新了,還要設計一個Ref類型。
測試一:針對以下測試代碼,Ref對象值的改變正確觸發(fā)了更新
?? ?//源碼位置 /packages/reactivity/__test__/ref.spec.ts ? ? const a = ref(1) ? ? let dummy ? ? let calls = 0 ? ? effect(() => { ? ? ? calls++ ? ? ? dummy = a.value; ? ? }) ? ? a.value = 2; ? ? //此時dummy = 2,a對象值的改變觸發(fā)了依賴更新
測試二:修改以上代碼,更新失敗
? ? const a = 1 ? ? let dummy ? ? let calls = 0 ? ? effect(() => { ? ? ? calls++ ? ? ? dummy = a; ? ? }) ? ? a= 2; ? ? //此時dummy = 1,a的改變沒有觸發(fā)依賴更新
上述2個示例很明顯的表明出了,對于非響應式對象的改變,不會觸發(fā)依賴更新。Reactive是通過代理實現(xiàn)的,代理只支持對象,不支持非對象的基礎類型。所以需要設計一個Ref類型來包裝這些類型數(shù)據(jù),以便擁有響應式狀態(tài)
2. 簡要說明
既然設計了Ref來支持非對象屬性,那么也一定需要兼容對象屬性。內(nèi)部其實很簡單,如果是對象,則直接轉(zhuǎn)為Reactive代理對象。
3. 關鍵源碼
class RefImpl<T> { ? private _value: T ? private _rawValue: T ? public dep?: Dep = undefined ? public readonly __v_isRef = true ? constructor(value: T, public readonly _shallow: boolean) { ? ? //原始數(shù)據(jù) ? ? this._rawValue = _shallow ? value : toRaw(value) ? ? //外部訪問到的數(shù)據(jù),轉(zhuǎn)換為響應式 ? ? this._value = _shallow ? value : toReactive(value) ? } ? get value() { ? ? //跟蹤依賴 ? ? trackRefValue(this) ? ? return this._value ? } ? set value(newVal) { ? ? newVal = this._shallow ? newVal : toRaw(newVal) ? ? if (hasChanged(newVal, this._rawValue)) { ? ? ? //如果原始數(shù)據(jù)之間的比較不一樣,則賦值 ? ? ? this._rawValue = newVal ? ? ? //把新值轉(zhuǎn)換為響應式對象 ? ? ? this._value = this._shallow ? newVal : toReactive(newVal) ? ? ? //觸發(fā)依賴 ? ? ? triggerRefValue(this, newVal) ? ? } ? } } //轉(zhuǎn)換響應式對象方法 export const toReactive = <T extends unknown>(value: T): T => ? isObject(value) ? reactive(value) : value type CustomRefFactory<T> = ( ? track: () => void, ? trigger: () => void ) => { ? get: () => T ? set: (value: T) => void } //收集依賴 export function trackRefValue(ref: RefBase<any>) { ? //是否可以收集 ? if (isTracking()) { ? ? //獲取原始數(shù)據(jù) ? ? ref = toRaw(ref) ? ? if (!ref.dep) { ? ? ? //如果不存在依賴,就創(chuàng)建一個依賴對象 ? ? ? ref.dep = createDep() ? ? } ? ? //收集依賴 ? ? if (__DEV__) { ? ? ? trackEffects(ref.dep, { ? ? ? ? target: ref, ? ? ? ? type: TrackOpTypes.GET, ? ? ? ? key: 'value' ? ? ? }) ? ? } else { ? ? ? trackEffects(ref.dep) ? ? } ? } } //觸發(fā)依賴更新 export function triggerRefValue(ref: RefBase<any>, newVal?: any) { ? ref = toRaw(ref) ? if (ref.dep) { ? ? if (__DEV__) { ? ? ? triggerEffects(ref.dep, { ? ? ? ? target: ref, ? ? ? ? type: TriggerOpTypes.SET, ? ? ? ? key: 'value', ? ? ? ? newValue: newVal ? ? ? }) ? ? } else { ? ? ? triggerEffects(ref.dep) ? ? } ? } } /** ?* 自定義響應式對象 ?*/ class CustomRefImpl<T> { ? public dep?: Dep = undefined ? private readonly _get: ReturnType<CustomRefFactory<T>>['get'] ? private readonly _set: ReturnType<CustomRefFactory<T>>['set'] ? public readonly __v_isRef = true ? constructor(factory: CustomRefFactory<T>) { ? ? const { get, set } = factory( ? ? ? () => trackRefValue(this), ? ? ? () => triggerRefValue(this) ? ? ) ? ? this._get = get ? ? this._set = set ? } ? get value() { ? ? return this._get() ? } ? set value(newVal) { ? ? this._set(newVal) ? } }
四. 源碼解析
Ref對象實際是代理的簡化版,針對value設置了一個getter,setter讀取器。
這個讀取器可以對讀寫操作進行攔截,因此可以進行依賴的收集和更新。
同時又巧妙了對reactive做了一層封裝,假如傳入的是一個多層嵌套的復雜對象,最終是類似ref.value.a其實操作的已經(jīng)是reactive代理對象上的屬性,已經(jīng)和ref無關了。對于CustomRefImpl類型,其實核心和RefImpl是一樣的,更加精簡,只不過將Get方法和Set方法交給程序員自己去實現(xiàn)了。
只需要在這個Get方法里面調(diào)用track方法進行依賴收集和在Set方法里面調(diào)用依賴更新即可。
示例代碼如下:
? ? let value = 1 ? ? const custom = customRef((track, trigger) => ({ ? ? ? get() { ? ? ? ? track() ? ? ? ? return value ? ? ? }, ? ? ? set(newValue: number) { ? ? ? ? value = newValue ? ? ? ? trigger() ? ? ? } ? ? })) ? ? let dummy ? ? effect(() => { ? ? ? dummy = custom.value? ? ? }) ? ? custom.value = 2 ? ? //此時dummy = 2;
五、總結
1. 收集依賴和觸發(fā)依賴的本質(zhì)
export const enum TrackOpTypes { ? GET = 'get', ? HAS = 'has', ? ITERATE = 'iterate' } export const enum TriggerOpTypes { ? SET = 'set', ? ADD = 'add', ? DELETE = 'delete', ? CLEAR = 'clear' }
以上時源碼中定義的收集依賴的和觸發(fā)依賴的類型。其實也就是當涉及讀操作時收集依賴,當設計寫操作時觸發(fā)依賴更新。
2. 響應式對象本質(zhì)是對數(shù)據(jù)進行了包裝,攔截了讀寫操作。
3. 上述篇幅并未講到集合類型代理的處理,原理其實一樣,有興趣的可以自行翻閱源碼。
4. 本篇幅只講到收集依賴和觸發(fā)依賴的時機,并未講到如何收集和如何觸發(fā)。
這些僅為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
vue?跳轉(zhuǎn)頁面$router.resolve和$router.push案例詳解
這篇文章主要介紹了vue?跳轉(zhuǎn)頁面$router.resolve和$router.push案例詳解,這樣實現(xiàn)了既跳轉(zhuǎn)了新頁面,又不會讓后端檢測到頁面鏈接不安全之類的,需要的朋友可以參考下2023-10-10vue.js實現(xiàn)的經(jīng)典計算器/科學計算器功能示例
這篇文章主要介紹了vue.js實現(xiàn)的經(jīng)典計算器/科學計算器功能,具有基本四則運算計算器以及科學計算器的功能,可實現(xiàn)開方、乘方、三角函數(shù)以及公式運算等功能,需要的朋友可以參考下2018-07-07Vue移動端實現(xiàn)pdf/excel/圖片在線預覽
這篇文章主要為大家詳細介紹了Vue移動端實現(xiàn)pdf/excel/圖片在線預覽功能的相關方法,文中的示例代碼講解詳細,有需要的小伙伴可以參考下2024-04-04