源碼視角,Vue3為什么推薦使用ref而不是reactive
共 13253字,需瀏覽 27分鐘
·
2024-08-02 09:15
點擊上方 前端Q,關(guān)注公眾號
回復加群,加入前端Q技術(shù)交流群
ref 和 reactive 是 Vue3 中實現(xiàn)響應(yīng)式數(shù)據(jù)的核心 API。ref 用于包裝基本數(shù)據(jù)類型,而 reactive 用于處理對象和數(shù)組。盡管 reactive 似乎更適合處理對象,但 Vue3 官方文檔更推薦使用 ref。
我的想法,ref就是比reactive好用,官方也是這么說的,不服來踩!下面我們從源碼的角度詳細討論這兩個 API,以及 Vue3 為什么推薦使用ref而不是reactive?
ref 的內(nèi)部工作原理
ref 是一個函數(shù),它接受一個內(nèi)部值并返回一個響應(yīng)式且可變的引用對象。這個引用對象有一個 .value 屬性,該屬性指向內(nèi)部值。
// 深響應(yīng)式
export function ref(value?: unknown) {
return createRef(value, false)
}
// 淺響應(yīng)式
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
function createRef(rawValue: unknown, shallow: boolean) {
// 如果傳入的值已經(jīng)是一個 ref,則直接返回它
if (isRef(rawValue)) {
return rawValue
}
// 否則,創(chuàng)建一個新的 RefImpl 實例
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
// 存儲響應(yīng)式的值。我們追蹤和更新的就是_value。(這個是重點)
private _value: T
// 用于存儲原始值,即未經(jīng)任何響應(yīng)式處理的值。(用于對比的,這塊的內(nèi)容可以不看)
private _rawValue: T
// 用于依賴跟蹤的 Dep 類實例
public dep?: Dep = undefined
// 一個標記,表示這是一個 ref 實例
public readonly __v_isRef = true
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
// 如果是淺響應(yīng)式,直接使用原始值,否則轉(zhuǎn)換為非響應(yīng)式原始值
this._rawValue = __v_isShallow ? value : toRaw(value)
// 如果是淺響應(yīng)式,直接使用原始值,否則轉(zhuǎn)換為響應(yīng)式值
this._value = __v_isShallow ? value : toReactive(value)
// toRaw 用于將響應(yīng)式引用轉(zhuǎn)換回原始值
// toReactive 函數(shù)用于將傳入的值轉(zhuǎn)換為響應(yīng)式對象。對于基本數(shù)據(jù)類型,toReactive 直接返回原始值。
// 對于對象和數(shù)組,toReactive 內(nèi)部會調(diào)用 reactive 來創(chuàng)建一個響應(yīng)式代理。
// 因此,對于 ref 來說,基本數(shù)據(jù)類型的值會被 RefImpl 直接包裝,而對象和數(shù)組
// 會被 reactive 轉(zhuǎn)換為響應(yīng)式代理,最后也會被 RefImpl 包裝。
// 這樣,無論是哪種類型的數(shù)據(jù),ref 都可以提供響應(yīng)式的 value 屬性,
// 使得數(shù)據(jù)變化可以被 Vue 正確追蹤和更新。
// export const toReactive = (value) => isObject(value) ? reactive(value) : value
}
get value() {
// 追蹤依賴,這樣當 ref 的值發(fā)生變化時,依賴這個 ref 的組件或副作用函數(shù)可以重新運行。
trackRefValue(this)
// 返回存儲的響應(yīng)式值
return this._value
}
set value(newVal) {
// 判斷是否應(yīng)該使用新值的直接形式(淺響應(yīng)式或只讀)
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
// 如果需要,將新值轉(zhuǎn)換為非響應(yīng)式原始值
newVal = useDirectValue ? newVal : toRaw(newVal)
// 如果新值與舊值不同,更新 _rawValue 和 _value
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 觸發(fā)依賴更新
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}
在上述代碼中,ref 函數(shù)通過 new RefImpl(value) 創(chuàng)建了一個新的 RefImpl 實例。這個實例包含 getter 和 setter,分別用于追蹤依賴和觸發(fā)更新。使用 ref 可以聲明任何數(shù)據(jù)類型的響應(yīng)式狀態(tài),包括對象和數(shù)組。
import { ref } from 'vue'
let state = ref({ count: 0 })
state.value.count++
注意,ref核心是返回「響應(yīng)式且可變的引用對象」,而reactive核心是返回的是「響應(yīng)式代理」,這是兩者本質(zhì)上的核心區(qū)別,也就導致了ref優(yōu)于reactive,我們接著看下reactive源碼實現(xiàn)。
reactive 的內(nèi)部工作原理
reactive 是一個函數(shù),它接受一個對象并返回該對象的響應(yīng)式代理,也就是 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) 創(chuàng)建了一個代理。這個代理會攔截對目標對象的操作,從而實現(xiàn)響應(yīng)式。
import { reactive } from 'vue'
let state = reactive({ count: 0 })
state.count++
到這里我們可以看出 ref 和 reactive 在聲明數(shù)據(jù)的響應(yīng)式狀態(tài)上,底層原理是不一樣的。ref 采用 RefImpl對象實例,reactive采用Proxy代理對象。
ref 更深入的理解
當你使用 new RefImpl(value) 創(chuàng)建一個 RefImpl 實例時,這個實例大致上會包含以下幾部分:
-
「內(nèi)部值」:實例存儲了傳遞給構(gòu)造函數(shù)的初始值。 -
「依賴收集」:實例需要跟蹤所有依賴于它的效果(effect),例如計算屬性或者副作用函數(shù)。這通常通過一個依賴列表或者集合來實現(xiàn)。 -
「觸發(fā)更新」:當實例的值發(fā)生變化時,它需要通知所有依賴于它的效果,以便它們可以重新計算或執(zhí)行。
RefImpl 類似于發(fā)布-訂閱模式的設(shè)計,以下是一個簡化的 RefImpl 類的偽代碼實現(xiàn),展示這個實現(xiàn)過程:
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;
// 值改變時,觸發(fā)更新
this.dep.notify();
}
}
}
// 使用示例
let count = new RefImpl(0);
watchEffect(() => {
console.log(`The count is: ${count.value}`); // 訂閱變化
});
count.value++; // 修改值,觸發(fā)通知,重新執(zhí)行watchEffect中的函數(shù)
Dep 類負責管理一個依賴列表,并提供依賴收集和通知更新的功能。RefImpl 類包含一個內(nèi)部值 _value 和一個 Dep 實例。當 value 被訪問時,通過 get 方法進行依賴收集;當 value 被賦予新值時,通過 set 方法觸發(fā)更新。
ref 和 reactive 盡管兩者在內(nèi)部實現(xiàn)上有所不同,但它們都能滿足我們對于聲明響應(yīng)式變量的要求,但是 reactive 卻存在一定的局限性。
reactive 的局限性
在 Vue3 中,reactive API 通過 Proxy 實現(xiàn)了一種響應(yīng)式數(shù)據(jù)的方法,盡管這種方法在性能上比 Vue2 有所提升,但 Proxy 的局限性也導致了 reactive 的局限性,這些局限性可能會影響開發(fā)者的使用體驗。
僅對引用數(shù)據(jù)類型有效
reactive 主要適用于對象,包括數(shù)組和一些集合類型(如 Map 和 Set)。對于基礎(chǔ)數(shù)據(jù)類型(如 string、number 和 boolean),reactive 是無效的。這意味著如果你嘗試使用 reactive 來處理這些基礎(chǔ)數(shù)據(jù)類型,將會得到一個非響應(yīng)式的對象。
import { reactive } from 'vue';
const state = reactive({ count: 0 });
使用不當會失去響應(yīng)
-
「直接賦值對象」:如果直接將一個響應(yīng)式對象賦值給另一個變量,將會失去響應(yīng)性。這是因為 reactive 返回的是對象本身,而不僅僅是代理。
import { reactive } from 'vue';
let state = reactive({ count: 0 });
state = { count: 1 }; // 失去響應(yīng)性 -
「直接替換響應(yīng)式對象」:同樣,直接替換一個響應(yīng)式對象也會導致失去響應(yīng)性。
import { reactive } from 'vue';
let state = reactive({ count: 0 });
state = reactive({ count: 1 }); // 失去響應(yīng)性 -
「直接解構(gòu)對象」:在解構(gòu)響應(yīng)式對象時,如果直接解構(gòu)對象屬性,將會得到一個非響應(yīng)式的變量。
const state = reactive({ count: 0 });
let { count } = state;
count++; // count 仍然是 0解決這個問題,需要使用
toRefs函數(shù)來將響應(yīng)式對象轉(zhuǎn)換為ref對象。import { toRefs } from 'vue';
const state = reactive({ count: 0 });
let { count } = toRefs(state);
count++; // count 現(xiàn)在是 1 -
「將響應(yīng)式對象的屬性賦值給變量」:如果將響應(yīng)式對象的屬性賦值給一個變量,這個變量的值將不會是響應(yīng)式的。
let state = reactive({ count: 0 })
let count = state.count
count++ // count 仍然是 0
console.log(state.count)
使用 reactive 聲明響應(yīng)式變量的確存在一些不便之處,尤其是對于喜歡使用解構(gòu)賦值的開發(fā)者而言。這些局限性可能會導致意外的行為,因此在使用 reactive 時需要格外注意。相比之下,ref API 提供了一種更靈活和統(tǒng)一的方式來處理響應(yīng)式數(shù)據(jù)。
為什么推薦使用 ref ?
ref()它為響應(yīng)式編程提供了一種統(tǒng)一的解決方案,適用于所有類型的數(shù)據(jù),包括基本數(shù)據(jù)類型和復雜對象。以下是推薦使用 ref 的幾個關(guān)鍵原因:
統(tǒng)一性
ref 的核心優(yōu)勢之一是它的統(tǒng)一性。它提供了一種簡單、一致的方式來處理所有類型的數(shù)據(jù),無論是數(shù)字、字符串、對象還是數(shù)組。這種統(tǒng)一性極大地簡化了開發(fā)者的代碼,減少了在不同數(shù)據(jù)類型之間切換時的復雜性。
import { ref } from 'vue';
let num = ref(0);
let str = ref('Hello');
let obj = ref({ count: 0 });
// 修改基本數(shù)據(jù)類型
num.value++;
str.value += ' World';
// 修改對象
obj.value.count++;
深層響應(yīng)性
ref 支持深層響應(yīng)性,這意味著它可以追蹤和更新嵌套對象和數(shù)組中的變化。這種特性使得 ref 非常適合處理復雜的數(shù)據(jù)結(jié)構(gòu),如對象和數(shù)組。
import { ref } from 'vue';
let obj = ref({
user: {
name: 'xiaoming',
details: {
age: 18
}
}
});
// 修改嵌套對象
obj.value.user.details.age++;
當然,為了減少大型不可變數(shù)據(jù)的響應(yīng)式開銷,也可以通過使用shallowRef來放棄深層響應(yīng)性。
let shallowObj = shallowRef({
details: { age: 18, },
});
靈活性
ref 提供了高度的靈活性,尤其在處理「普通賦值」方面。這種靈活性使得 ref 在開發(fā)中的使用更加方便,特別是在進行復雜的數(shù)據(jù)操作時。
import { ref } from 'vue';
let state = ref({
count: 0,
name: 'Vue'
});
// 替換整個對象
state.value = {
count: 10,
name: 'Vue 4'
};
// 修改對象內(nèi)的屬性
state.value.count = 20;
state.value.name = 'Vue 5';
// 添加新的屬性
state.value.newProperty = 'New Property';
// 刪除屬性
delete state.value.newProperty;
// 使用解構(gòu)更新屬性(注意要保持響應(yīng)性)
let { count, name } = state.value;
state.value = { count: count + 1, name };
// 復雜操作,例如根據(jù)條件更新屬性
if (someCondition) {
state.value = {
...state.value,
name: 'Updated Name'
};
}
console.log(state.value)
總結(jié)
ref 在 Vue3 中提供了一種更統(tǒng)一、靈活的響應(yīng)式解決方案,還能避免了 reactive 的某些局限性。希望這篇文章對你有所幫助,有所借鑒。大家怎么認為呢,評論區(qū)我們一起討論下!
往期推薦
最后
歡迎加我微信,拉你進技術(shù)群,長期交流學習...
歡迎關(guān)注「前端Q」,認真學前端,做個專業(yè)的技術(shù)人...
