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 = trueconstructor(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 和 _valueif (hasChanged(newVal, this._rawValue)) {this._rawValue = newValthis._value = useDirectValue ? newVal : toReactive(newVal)// 觸發依賴更新triggerRefValue(this, DirtyLevels.Dirty, newVal)}}
}
在上述代碼中,ref
?函數通過?new RefImpl(value)
?創建了一個新的?RefImpl
?實例。這個實例包含 getter 和 setter,分別用于追蹤依賴和觸發更新。使用?ref
?可以聲明任何數據類型的響應式狀態,包括對象和數組。
import { ref } from 'vue' let 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' let 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();}}
}// 使用示例
let 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';let state = reactive({ count: 0 }); state = { count: 1 }; // 失去響應性
-
直接替換響應式對象:同樣,直接替換一個響應式對象也會導致失去響應性。
import { reactive } from 'vue';let 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
-
將響應式對象的屬性賦值給變量:如果將響應式對象的屬性賦值給一個變量,這個變量的值將不會是響應式的。
let state = reactive({ count: 0 })let count = state.count count++ // count 仍然是 0 console.log(state.count)
使用?reactive
?聲明響應式變量的確存在一些不便之處,尤其是對于喜歡使用解構賦值的開發者而言。這些局限性可能會導致意外的行為,因此在使用?reactive
?時需要格外注意。相比之下,ref
?API 提供了一種更靈活和統一的方式來處理響應式數據。
為什么推薦使用 ref ?
ref()
它為響應式編程提供了一種統一的解決方案,適用于所有類型的數據,包括基本數據類型和復雜對象。以下是推薦使用 ref 的幾個關鍵原因:
統一性
ref
?的核心優勢之一是它的統一性。它提供了一種簡單、一致的方式來處理所有類型的數據,無論是數字、字符串、對象還是數組。這種統一性極大地簡化了開發者的代碼,減少了在不同數據類型之間切換時的復雜性。
import { ref } from 'vue';let num = ref(0);
let str = ref('Hello');
let obj = ref({ count: 0 });// 修改基本數據類型
num.value++;
str.value += ' World';// 修改對象
obj.value.count++;
深層響應性
ref
?支持深層響應性,這意味著它可以追蹤和更新嵌套對象和數組中的變化。這種特性使得?ref
?非常適合處理復雜的數據結構,如對象和數組。
import { ref } from 'vue';let obj = ref({user: {name: 'xiaoming',details: {age: 18}}
});// 修改嵌套對象
obj.value.user.details.age++;
當然,為了減少大型不可變數據的響應式開銷,也可以通過使用shallowRef
來放棄深層響應性。
let shallowObj = shallowRef({ details: { age: 18, },
});
靈活性
ref
?提供了高度的靈活性,尤其在處理普通賦值方面。這種靈活性使得 ref 在開發中的使用更加方便,特別是在進行復雜的數據操作時。
import { ref } from 'vue';let state = ref({count: 0,name: 'Vue'
});// 替換整個對象
state.value = {count: 10,name: 'Vue 4'
};
// 修改對象內的屬性
state.value.count = 20;
state.value.name = 'Vue 5';
// 添加新的屬性
state.value.newProperty = 'New Property';
// 刪除屬性
delete state.value.newProperty;
// 使用解構更新屬性(注意要保持響應性)
let { count, name } = state.value;
state.value = { count: count + 1, name };
// 復雜操作,例如根據條件更新屬性
if (someCondition) {state.value = {...state.value,name: 'Updated Name'};
}
console.log(state.value)
總結
ref
?在 Vue3 中提供了一種更統一、靈活的響應式解決方案,還能避免了?reactive
?的某些局限性。希望這篇文章對你有所幫助,有所借鑒。大家怎么認為呢,評論區我們一起討論下!
?