這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 ref 和 reactive 是 Vue3 中實現響應式數據的核心 API。ref 用於包裝基本數據類型,而 reactive 用於處理對象和數組。儘管 reactive 似乎更適合處理對象,但 Vue3 官方文檔更推薦使用 ref。 我 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
ref
和 reactive
是 Vue3 中實現響應式數據的核心 API。ref
用於包裝基本數據類型,而 reactive 用於處理對象和數組。儘管 reactive
似乎更適合處理對象,但 Vue3 官方文檔更推薦使用 ref
。
我的想法,ref
就是比reactive
好用,官方也是這麼說的,不服來踩!下麵我們從源碼的角度詳細討論這兩個 API,以及 Vue3 為什麼推薦使用ref
而不是reactive
?
ref 的內部工作原理
ref
是一個函數,它接受一個內部值並返回一個響應式且可變的引用對象。這個引用對象有一個 .value
屬性,該屬性指向內部值。
// 深響應式 export function ref(value?: unknown) { return createRef(value, false) } // 淺響應式 export function shallowRef(value?: unknown) { return createRef(value, true) } function createRef(rawValue: unknown, shallow: boolean) { // 如果傳入的值已經是一個 ref,則直接返回它 if (isRef(rawValue)) { return rawValue } // 否則,創建一個新的 RefImpl 實例 return new RefImpl(rawValue, shallow) } class RefImpl<T> { // 存儲響應式的值。我們追蹤和更新的就是_value。(這個是重點) private _value: T // 用於存儲原始值,即未經任何響應式處理的值。(用於對比的,這塊的內容可以不看) private _rawValue: T // 用於依賴跟蹤的 Dep 類實例 public dep?: Dep = undefined // 一個標記,表示這是一個 ref 實例 public readonly __v_isRef = true constructor( value: T, public readonly __v_isShallow: boolean, ) { // 如果是淺響應式,直接使用原始值,否則轉換為非響應式原始值 this._rawValue = __v_isShallow ? value : toRaw(value) // 如果是淺響應式,直接使用原始值,否則轉換為響應式值 this._value = __v_isShallow ? value : toReactive(value) // toRaw 用於將響應式引用轉換回原始值 // toReactive 函數用於將傳入的值轉換為響應式對象。對於基本數據類型,toReactive 直接返回原始值。 // 對於對象和數組,toReactive 內部會調用 reactive 來創建一個響應式代理。 // 因此,對於 ref 來說,基本數據類型的值會被 RefImpl 直接包裝,而對象和數組 // 會被 reactive 轉換為響應式代理,最後也會被 RefImpl 包裝。 // 這樣,無論是哪種類型的數據,ref 都可以提供響應式的 value 屬性, // 使得數據變化可以被 Vue 正確追蹤和更新。 // export const toReactive = (value) => isObject(value) ? reactive(value) : value } get value() { // 追蹤依賴,這樣當 ref 的值發生變化時,依賴這個 ref 的組件或副作用函數可以重新運行。 trackRefValue(this) // 返回存儲的響應式值 return this._value } set value(newVal) { // 判斷是否應該使用新值的直接形式(淺響應式或只讀) const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal) // 如果需要,將新值轉換為非響應式原始值 newVal = useDirectValue ? newVal : toRaw(newVal) // 如果新值與舊值不同,更新 _rawValue 和 _value if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = useDirectValue ? newVal : toReactive(newVal) // 觸發依賴更新 triggerRefValue(this, DirtyLevels.Dirty, newVal) } } }
在上述代碼中,ref
函數通過 new RefImpl(value)
創建了一個新的 RefImpl
實例。這個實例包含 getter 和 setter,分別用於追蹤依賴和觸發更新。使用 ref
可以聲明任何數據類型的響應式狀態,包括對象和數組。
import { ref } from 'vue' const state = ref({ count: 0 }) state.value.count++
註意,ref
核心是返回響應式且可變的引用對象,而reactive
核心是返回的是響應式代理,這是兩者本質上的核心區別,也就導致了ref
優於reactive
,我們接著看下reactive
源碼實現。
reactive 的內部工作原理
reactive
是一個函數,它接受一個對象並返回該對象的響應式代理,也就是 Proxy
。
function reactive(target) { if (target && target.__v_isReactive) { return target } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ) } function createReactiveObject( target, isReadonly, baseHandlers, collectionHandlers, proxyMap ) { if (!isObject(target)) { return target } const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } const proxy = new Proxy(target, baseHandlers) proxyMap.set(target, proxy) return proxy }
reactive
的源碼相對就簡單多了,reactive
通過 new Proxy(target, baseHandlers)
創建了一個代理。這個代理會攔截對目標對象的操作,從而實現響應式。import { reactive } from 'vue' const state = reactive({ count: 0 }) state.count++
到這裡我們可以看出 ref
和 reactive
在聲明數據的響應式狀態上,底層原理是不一樣的。ref
採用 RefImpl
對象實例,reactive
採用Proxy
代理對象。
ref 更深入的理解
當你使用 new RefImpl(value)
創建一個 RefImpl
實例時,這個實例大致上會包含以下幾部分:
- 內部值:實例存儲了傳遞給構造函數的初始值。
- 依賴收集:實例需要跟蹤所有依賴於它的效果(effect),例如計算屬性或者副作用函數。這通常通過一個依賴列表或者集合來實現。
- 觸發更新:當實例的值發生變化時,它需要通知所有依賴於它的效果,以便它們可以重新計算或執行。
RefImpl 類似於發佈-訂閱模式的設計,以下是一個簡化的 RefImpl
類的偽代碼實現,展示這個實現過程:
class Dep { constructor() { this.subscribers = new Set(); } depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } notify() { this.subscribers.forEach(effect => effect()); } } let activeEffect = null; function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } class RefImpl { constructor(value) { this._value = value; this.dep = new Dep(); } get value() { // 當獲取值時,進行依賴收集 this.dep.depend(); return this._value; } set value(newValue) { if (newValue !== this._value) { this._value = newValue; // 值改變時,觸發更新 this.dep.notify(); } } } // 使用示例 const count = new RefImpl(0); watchEffect(() => { console.log(`The count is: ${count.value}`); // 訂閱變化 }); count.value++; // 修改值,觸發通知,重新執行watchEffect中的函數Dep 類負責管理一個依賴列表,並提供依賴收集和通知更新的功能。RefImpl 類包含一個內部值 _value 和一個 Dep 實例。當 value 被訪問時,通過 get 方法進行依賴收集;當 value 被賦予新值時,通過 set 方法觸發更新。
ref
和 reactive
儘管兩者在內部實現上有所不同,但它們都能滿足我們對於聲明響應式變數的要求,但是 reactive
卻存在一定的局限性。
reactive 的局限性
在 Vue3 中,reactive
API 通過 Proxy
實現了一種響應式數據的方法,儘管這種方法在性能上比 Vue2 有所提升,但 Proxy
的局限性也導致了 reactive
的局限性,這些局限性可能會影響開發者的使用體驗。
僅對引用數據類型有效
reactive
主要適用於對象,包括數組和一些集合類型(如 Map
和 Set
)。對於基礎數據類型(如 string
、number
和 boolean
),reactive
是無效的。這意味著如果你嘗試使用 reactive
來處理這些基礎數據類型,將會得到一個非響應式的對象。
import { reactive } from 'vue'; const state = reactive({ count: 0 });
使用不當會失去響應
-
直接賦值對象:如果直接將一個響應式對象賦值給另一個變數,將會失去響應性。這是因為 reactive 返回的是對象本身,而不僅僅是代理。
import { reactive } from 'vue'; const state = reactive({ count: 0 }); state = { count: 1 }; // 失去響應性
- 直接替換響應式對象:同樣,直接替換一個響應式對象也會導致失去響應性。
import { reactive } from 'vue'; const state = reactive({ count: 0 }); state = reactive({ count: 1 }); // 失去響應性
- 直接解構對象:在解構響應式對象時,如果直接解構對象屬性,將會得到一個非響應式的變數。
const state = reactive({ count: 0 }); let { count } = state; count++; // count 仍然是 0好家伙!常用的解構賦值不能用。為瞭解決這個問題,需要使用
toRefs
函數來將響應式對象轉換為 ref
對象。import { toRefs } from 'vue'; const state = reactive({ count: 0 }); let { count } = toRefs(state); count++; // count 現在是 1
首先來說,太不方便了!而且使用toRefs()
,將響應式變數換成 ref 的形式,那我還不如直接使用ref()
了,大家說是不是?
-
將響應式對象的屬性賦值給變數:如果將響應式對象的屬性賦值給一個變數,這個變數的值將不會是響應式的。
const state = reactive({ count: 0 }) let count = state.count count++ // count 仍然是 0
使用 reactive
聲明響應式變數的確存在一些不便之處,尤其是對於喜歡使用解構賦值的開發者而言。這些局限性可能會導致意外的行為,因此在使用 reactive
時需要格外註意。相比之下,ref
API 提供了一種更靈活和統一的方式來處理響應式數據。
為什麼推薦使用 ref ?
ref()
它為響應式編程提供了一種統一的解決方案,適用於所有類型的數據,包括基本數據類型和複雜對象。以下是推薦使用 ref 的幾個關鍵原因:
統一性
ref
的核心優勢之一是它的統一性。它提供了一種簡單、一致的方式來處理所有類型的數據,無論是數字、字元串、對象還是數組。這種統一性極大地簡化了開發者的代碼,減少了在不同數據類型之間切換時的複雜性。
import { ref } from 'vue'; const num = ref(0); const str = ref('Hello'); const obj = ref({ count: 0 }); // 修改基本數據類型 num.value++; str.value += ' World'; // 修改對象 obj.value.count++;
深層響應性
ref
支持深層響應性,這意味著它可以追蹤和更新嵌套對象和數組中的變化。這種特性使得 ref
非常適合處理複雜的數據結構,如對象和數組。
import { ref } from 'vue'; const obj = ref({ user: { name: 'xiaoming', details: { age: 18 } } }); // 修改嵌套對象 obj.value.user.details.age++;當然,為了減少大型不可變數據的響應式開銷,也可以通過使用
shallowRef
來放棄深層響應性。const shallowObj = shallowRef({ details: { age: 18, }, });
靈活性
ref
提供了高度的靈活性,尤其在處理普通賦值和解構賦值方面。這種靈活性使得 ref 在開發中的使用更加方便,特別是在進行複雜的數據操作時。
import { ref } from 'vue'; const state = ref({ count: 0, name: 'Vue' }); // 解構賦值 const { count, name } = state.value; // 直接修改解構後的變數 count++; name = 'Vue3'; // 替換整個對象 state.value = { count: 10, name: 'Vue4' };
總結
ref
在 Vue3 中提供了一種更統一、靈活的響應式解決方案,還能避免了 reactive
的某些局限性。希望這篇文章對你有所幫助,有所借鑒。大家怎麼認為呢,評論區我們一起討論下!