您好,登錄后才能下訂單哦!
這篇“vue3中ref和reactive怎么使用”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“vue3中ref和reactive怎么使用”文章吧。
vue3新增了ref,reactive兩個api用于響應式數據,Ref 系列毫無疑問是使用頻率最高的 api 之一,響應式意味著數據變動,頁面局部自動更新。數據類型有基本數據類型(string,number,boolean,undfined,null,symbol),引用數據類型(object,array,set,map等)。如何精準檢測跟蹤js中所有的數據類型變動,并且能夠達到vnode的對比后真實dom的渲染?vue中是如何做到的呢?簡單實例如下:
import { reactive, ref } from "vue"; import type { Ref } from "vue"; // 定義響應式數據 const count: Ref<number> = ref(0); function countClick() { count.value++; // 更新數據 }
// 定義引用類型數據標注 interface TypeForm { name: string; num: number; list?: Array<[]>; } const formInline: TypeForm = reactive({ name: "", num: 0, }); formInline.name = 'KinHKin' formInline.num = 100 formInline.list = [1,2,3,4]
效果圖:
先做個ref和reactive的比較
不推薦使用
reactive()
的泛型參數,因為處理了深層次 ref 解包的返回值與泛型參數的類型不同。ref 被傳遞給函數或是從一般對象上被解構時,不會丟失響應性:
const obj = { foo: ref(1), bar: ref(2) } // 該函數接收一個 ref // 需要通過 .value 取值 // 但它會保持響應性 callSomeFunction(obj.foo) // 仍然是響應式的 const { foo, bar } = obj
簡而言之,ref() 讓我們能創造一種對任意值的 “引用”,并能夠在不丟失響應性的前提下傳遞這些引用。這個功能很重要,因為它經常用于將邏輯提取到 組合函數 中。
當 ref 在模板中作為頂層屬性被訪問時,它們會被自動“解包”,所以不需要使用 .value。下面是之前的計數器例子,用 ref() 代替:
<script setup> import { ref } from 'vue' const count = ref(0) function increment() { count.value++ } </script> <template> <button @click="increment"> {{ count }} <!-- 無需 .value --> </button> </template>
請注意,僅當 ref 是模板渲染上下文的頂層屬性時才適用自動“解包”。
對于vue3.2.2x版本的源碼位于node_moudles/@vue/reactivity/dist/reactivity.cjs.js文件中
執行順序是ref ->createRef ->new RefImpl 生成實例對象,提供get,set方法
源碼中我們可以看到:入口有兩個函數默認深層次響應ref,淺層次使用shallowRef,參數一個false,一個是true。
function ref(value) { return createRef(value, false); } function shallowRef(value) { return createRef(value, true); }
接下來就是走createRef這個方法:
function createRef(rawValue, shallow) { if (isRef(rawValue)) { return rawValue; } return new RefImpl(rawValue, shallow); }
這個createRef方法接受兩個參數,一個是傳入的基本類型的默認數值,一個是否是深層次響應的boolean值。
function isRef(r) { return !!(r && r.__v_isRef === true); }
如果rawValue本就是ref類型的會立即返回rawValue,否則返回一個RefImpl實例。
RefImpl類:
class RefImpl { constructor(value, __v_isShallow) { this.__v_isShallow = __v_isShallow; this.dep = undefined; this.__v_isRef = true; 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 (shared.hasChanged(newVal, this._rawValue)) { this._rawValue = newVal; this._value = useDirectValue ? newVal : toReactive(newVal); triggerRefValue(this, newVal); } } }
RefImpl類在構造函數中,__v_isShallow表示是否是淺層次響應的屬性, 私有的 _rawValue 變量,存放 ref 的舊值,_value是ref接受的最新的值。公共的只讀變量 __v_isRef 是用來標識該對象是一個 ref 響應式對象的標記與在講述 reactive api 時的 ReactiveFlag 相同。
在const toReactive = (value) => shared.isObject(value) ? reactive(value) : value;這個函數的內部判斷是否傳入的是一個對象,如果是一個對象就返回reactive返回代理對象,否則直接返回原參數。
當我們通過 ref.value 的形式讀取該 ref 的值時,就會觸發 value 的 getter 方法,在 getter 中會先通過 trackRefValue 收集該 ref 對象的 value 的依賴,收集完畢后返回該 ref 的值。
function trackRefValue(ref) { if (shouldTrack && activeEffect) { ref = toRaw(ref); { trackEffects(ref.dep || (ref.dep = createDep()), { target: ref, type: "get" /* TrackOpTypes.GET */, key: 'value' }); } } }
當我們對 ref.value 進行修改時,又會觸發 value 的 setter 方法,會將新舊 value 進行比較,如果值不同需要更新,則先更新新舊 value,之后通過 triggerRefValue 派發該 ref 對象的 value 屬性的更新,讓依賴該 ref 的副作用函數執行更新。
function triggerRefValue(ref, newVal) { ref = toRaw(ref); if (ref.dep) { { triggerEffects(ref.dep, { target: ref, type: "set" /* TriggerOpTypes.SET */, key: 'value', newValue: newVal }); } } }
對于vue3.2.2x版本的源碼位于node_moudles/@vue/reactivity/dist/reactivity.cjs.js文件中
整體描述vue3的更新機制:
在 Vue3 中,通過 track 的處理器函數來收集依賴,通過 trigger 的處理器函數來派發更新,每個依賴的使用都會被包裹到一個副作用(effect)函數中,而派發更新后就會執行副作用函數,這樣依賴處的值就被更新了。
Proxy 對象能夠利用 handler 陷阱在 get、set 時捕獲到任何變動,也能監聽對數組索引的改動以及 數組 length 的改動。
執行順序是:reactive -> createReactiveObject ->
function reactive(target) { // if trying to observe a readonly proxy, return the readonly version. if (isReadonly(target)) { return target; } return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap); }
第三行 isReadonly 函數 確定對象是否為只讀對象,IS_READONLY key 確定對象是否為只讀對象。ReactiveFlags 枚舉會在源碼中不斷的與我們見面,所以有必要提前介紹一下 ReactiveFlags:
function isReadonly(value) { return !!(value && value["__v_isReadonly" /* ReactiveFlags.IS_READONLY */]); }
export const enum ReactiveFlags { SKIP = '__v_skip', // 是否跳過響應式 返回原始對象 IS_REACTIVE = '__v_isReactive', // 標記一個響應式對象 IS_READONLY = '__v_isReadonly', // 標記一個只讀對象 RAW = '__v_raw' // 標記獲取原始值 IS_SHALLOW = '__v_isShallow' // 是否淺層次拷貝 }
在 ReactiveFlags 枚舉中有 5 個枚舉值,這五個枚舉值的含義都在注釋里。對于 ReactiveFlags 的使用是代理對象對 handler 中的 trap 陷阱非常好的應用,對象中并不存在這些 key,而通過 get 訪問這些 key 時,返回值都是通過 get 陷阱的函數內處理的。介紹完 ReactiveFlags 后我們繼續往下看。
入參部分:
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {}
先看 createReactiveObject 函數的簽名,該函數接受 5 個參數:
1、target:目標對象,想要生成響應式的原始對象。
2、isReadonly:生成的代理對象是否只讀。
3、baseHandlers:生成代理對象的 handler 參數。當 target 類型是 Array 或 Object 時使用該 handler。
4、collectionHandlers:當 target 類型是 Map、Set、WeakMap、WeakSet 時使用該 handler。
5、proxyMap:存儲生成代理對象后的 Map 對象。
這里需要注意的是 baseHandlers 和 collectionHandlers 的區別,這兩個參數會根據 target 的類型進行判斷,最終選擇將哪個參數傳入 Proxy 的構造函數,當做 handler 參數使用。
邏輯部分:
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) { // 如何不是對象 曝出警告 返回其原始值 if (!shared.isObject(target)) { { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } // target is already a Proxy, return it. // exception: calling readonly() on a reactive object // 如果目標已經是一個代理,直接返回 KinHKin譯 // 除非對一個響應式對象執行 readonly if (target["__v_raw" /* ReactiveFlags.RAW */] && !(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) { return target; } // target already has corresponding Proxy // 目標已經存在對應的代理對象 KinHKin譯 const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } // only specific value types can be observed. // 只有白名單里的類型才能被創建響應式對象 KinHKin譯 const targetType = getTargetType(target); if (targetType === 0 /* TargetType.INVALID */) { return target; } const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers); proxyMap.set(target, proxy); return proxy; }
在該函數的邏輯部分,可以看到基礎數據類型并不會被轉換成代理對象,而是直接返回原始值。
并且會將已經生成的代理對象緩存進傳入的 proxyMap,當這個代理對象已存在時不會重復生成,會直接返回已有對象。
也會通過 TargetType 來判斷 target 目標對象的類型,Vue3 僅會對 Array、Object、Map、Set、WeakMap、WeakSet 生成代理,其他對象會被標記為 INVALID,并返回原始值。
當目標對象通過類型校驗后,會通過 new Proxy() 生成一個代理對象 proxy,handler 參數的傳入也是與 targetType 相關,并最終返回已生成的 proxy 對象。
所以回顧 reactive api,我們可能會得到一個代理對象,也可能只是獲得傳入的 target 目標對象的原始值。
在 @vue/reactive 庫中有 baseHandlers 和 collectionHandlers 兩個模塊,分別生成 Proxy 代理的 handlers 中的 trap 陷阱。
例如在上面生成 reactive 的 api 中 baseHandlers 的參數傳入了一個 mutableHandlers 對象,這個對象是這樣的:
const mutableHandlers = { get, set, deleteProperty, has, ownKeys };
通過變量名我們能知道 mutableHandlers 中存在 5 個 trap 陷阱。而在 baseHandlers 中,get 和 set 都是通過工廠函數生成的,以便于適配除 reactive 外的其他 api,例如 readonly、shallowReactive、shallowReadonly 等。
baseHandlers 是處理 Array、Object 的數據類型的,這也是我們絕大部分時間使用 Vue3 時使用的類型,所以筆者接下來著重的講一下baseHandlers 中的 get 和 set 陷阱。
上一段提到 get 是由一個工廠函數生成的,先來看一下 get 陷阱的種類。
const get = /*#__PURE__*/ createGetter(); const shallowGet = /*#__PURE__*/ createGetter(false, true); const readonlyGet = /*#__PURE__*/ createGetter(true); const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true);
函數內部返回一個get函數,使用了閉包的方式,將get函數中的參數傳到handlers中。
createGetter 的邏輯:
function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { // 如果key是響應式的對象 就返回不是只讀 *KinHKin注釋* if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) { return !isReadonly; } // 如果key是只讀對象 就返回只讀是true *KinHKin注釋* else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) { return isReadonly; } // 如果key是淺層次響應對象 就返回淺層次是true *KinHKin注釋* else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) { return shallow; } // 如果key是原始值對象并且改變的值和原始標記一致 就返回原始值 *KinHKin注釋* else if (key === "__v_raw" /* ReactiveFlags.RAW */ && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target)) { return target; } // 判斷傳入的值是不是數組 const targetIsArray = shared.isArray(target); // 如果不是只讀 并且是數組 // arrayInstrumentations 是一個對象,對象內保存了若干個被特殊處理的數組方法,并以鍵值對的形式存儲。 *KinHKin注釋* if (!isReadonly && targetIsArray && shared.hasOwn(arrayInstrumentations, key)) { // 特殊處理數組返回結果 return Reflect.get(arrayInstrumentations, key, receiver); } // 獲取 Reflect 執行的 get 默認結果 const res = Reflect.get(target, key, receiver); // 如果是 key 是 Symbol,并且 key 是 Symbol 對象中的 Symbol 類型的 key // 或者 key 是不需要追蹤的 key: __proto__,__v_isRef,__isVue // 直接返回 get 結果 *KinHKin注釋* if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res; } // 不是只讀對象 執行 track 收集依賴 *KinHKin注釋* if (!isReadonly) { track(target, "get" /* TrackOpTypes.GET */, key); } // 是淺層次響應 直接返回 get 結果 *KinHKin注釋* if (shallow) { return res; } if (isRef(res)) { // ref unwrapping - skip unwrap for Array + integer key. // 如果是 ref ,則返回解包后的值 - 當 target 是數組,key 是 int 類型時,不需要解包 *KinHKin注釋* return targetIsArray && shared.isIntegerKey(key) ? res : res.value; } if (shared.isObject(res)) { // 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. // 將返回的值也轉換成代理,我們在這里做 isObject 的檢查以避免無效值警告。 // 也需要在這里惰性訪問只讀和星影視對象,以避免循環依賴。*KinHKin注釋* return isReadonly ? readonly(res) : reactive(res); } // 不是 object 類型則直接返回 get 結果 *KinHKin注釋* return res; }; }
從這段 createGetter 邏輯中,之前專門介紹過的 ReactiveFlags 枚舉在這就取得了妙用。其實目標對象中并沒有這些 key,但是在 get 中Vue3 就對這些 key 做了特殊處理,當我們在對象上訪問這幾個特殊的枚舉值時,就會返回特定意義的結果。而可以關注一下 ReactiveFlags.IS_REACTIVE 這個 key 的判斷方式,為什么是只讀標識的取反呢?因為當一個對象的訪問能觸發這個 get 陷阱時,說明這個對象必然已經是一個 Proxy 對象了,所以只要不是只讀的,那么就可以認為是響應式對象了。
get 的后續邏輯:
繼續判斷 target 是否是一個數組,如果代理對象不是只讀的,并且 target 是一個數組,并且訪問的 key 在數組需要特殊處理的方法里,就會直接調用特殊處理的數組函數執行結果,并返回。
arrayInstrumentations 是一個對象,對象內保存了若干個被特殊處理的數組方法,并以鍵值對的形式存儲。
我們之前說過 Vue2 以原型鏈的方式劫持了數組,而在這里也有類似地作用,下面是需要特殊處理的數組。
對索引敏感的數組方法
includes、indexOf、lastIndexOf
會改變自身長度的數組方法,需要避免 length 被依賴收集,因為這樣可能會造成循環引用
push、pop、shift、unshift、splice
下面的幾個key是不需要被依賴收集或者是返回響應式結果的:
__proto__
_v_isRef
__isVue
在處理完數組后,我們對 target 執行 Reflect.get 方法,獲得默認行為的 get 返回值。
之后判斷 當前 key 是否是 Symbol,或者是否是不需要追蹤的 key,如果是的話直接返回 get 的結果 res。
接著判斷當前代理對象是否是只讀對象,如果不是只讀的話,則運行筆者上文提及的 tarck 處理器函數收集依賴。
如果是 shallow 的淺層響應式,則不需要將內部的屬性轉換成代理,直接返回 res。
如果 res 是一個 Ref 類型的對象,就會自動解包返回,這里就能解釋官方文檔中提及的 ref 在 reactive 中會自動解包的特性了。而需要注意的是,當 target 是一個數組類型,并且 key 是 int 類型時,即使用索引訪問數組元素時,不會被自動解包。
如果 res 是一個對象,就會將該對象轉成響應式的 Proxy 代理對象返回,再結合我們之前分析的緩存已生成的 proxy 對象,可以知道這里的邏輯并不會重復生成相同的 res,也可以理解文檔中提及的當我們訪問 reactive 對象中的 key 是一個對象時,它也會自動的轉換成響應式對象,而且由于在此處生成 reactive 或者 readonly 對象是一個延遲行為,不需要在第一時間就遍歷 reactive 傳入的對象中的所有 key,也對性能的提升是一個幫助。
當 res 都不滿足上述條件時,直接返回 res 結果。例如基礎數據類型就會直接返回結果,而不做特殊處理。最后,get 陷阱的邏輯全部結束了。
set 也有一個 createSetter 的工廠函數,也是通過柯里化的方式返回一個 set 函數。
set 的函數比較簡短,所以這次一次性把寫好注釋的代碼放上來,先看代碼再講邏輯。
// 純函數 默認深層次響應 函數不入參 *KinHKin* const set = /*#__PURE__*/ createSetter(); // 純函數 淺層次響應 函數入參是true *KinHKin* const shallowSet = /*#__PURE__*/ createSetter(true); function createSetter(shallow = false) { return function set(target, key, value, receiver) { let oldValue = target[key]; // 如果原始值是只讀and是ref類型and新的value屬性不是ref類型 直接返回 if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) { return false; } if (!shallow) { // 如果新的值不是淺層次響應對象,也不是只讀 更新舊值 新值為普通對象 *KinHKin* if (!isShallow(value) && !isReadonly(value)) { oldValue = toRaw(oldValue); value = toRaw(value); } // 當不是 只讀 模式時,判斷舊值是否是 Ref,如果是則直接更新舊值的 value // 因為 ref 有自己的 setter *KinHKin* if (!shared.isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } } // 判斷 target 中是否存在 key *KinHKin* const hadKey = shared.isArray(target) && shared.isIntegerKey(key) ? Number(key) < target.length : shared.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 // 如果目標是原始對象原型鏈上的屬性,則不會觸發 trigger 派發更新 *KinHKin* if (target === toRaw(receiver)) { // 使用 trigger 派發更新,根據 hadKey 區別調用事件 if (!hadKey) { trigger(target, "add" /* TriggerOpTypes.ADD */, key, value); } else if (shared.hasChanged(value, oldValue)) { trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue); } } return result; }; }
在 set 的過程中會首先獲取新舊與舊值,當目前的代理對象不是淺層比較時,會判斷舊值是否是一個 Ref,如果舊值不是數組且是一個 ref類型的對象,并且新值不是 ref 對象時,會直接修改舊值的 value。
看到這里可能會有疑問,為什么要更新舊值的 value?如果你使用過 ref 這個 api 就會知道,每個 ref 對象的值都是放在 value 里的,而 ref 與 reactive 的實現是有區別的,ref 其實是一個 class 實例,它的 value 有自己的 set ,所以就不會在這里繼續進行 set 了。
在處理完 ref 類型的值后,會聲明一個變量 hadKey,判斷當前要 set 的 key 是否是對象中已有的屬性。
接下來調用 Reflect.set 獲取默認行為的 set 返回值 result。
然后會開始派發更新的過程,在派發更新前,需要保證 target 和原始的 receiver 相等,target 不能是一個原型鏈上的屬性。
之后開始使用 trigger 處理器函數派發更新,如果 hadKey 不存在,則是一個新增屬性,通過 TriggerOpTypes.ADD 枚舉來標記。這里可以看到開篇分析 Proxy 強于 Object.defineProperty 的地方,會監測到任何一個新增的 key,讓響應式系統更強大。
如果 key 是當前 target 上已經存在的屬性,則比較一下新舊值,如果新舊值不一樣,則代表屬性被更新,通過 TriggerOpTypes.SET 來標記派發更新。
在更新派發完后,返回 set 的結果 result,至此 set 結束。
以上就是關于“vue3中ref和reactive怎么使用”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。