最近有朋友在面試過程中經常被問到這么一個問題,vue3 中的ref 和 reactive的區別在哪里,為什么 要定義兩個API 一個 api不能實現 響應式更新嗎??
帶著這個疑問 ,我們 接下來進行逐一探討。
1 : 分析
1.1 ref and reactive 怎么用 ?
相信大家都知道在vue3中我們可以通過一些api去定義響應式數據,比如 ref, reactive ,shallowRef.
ref 基本使用
<template>
<div>
<span>{{inner.content.text}}</span>
<span>{{count}}</span>
</div>
</template>
<script setup>
const inner = ref({
content: {
text: "內部內容"
}
}); // 你可以通過 ref 定義復雜數據類型
// or
const count = ref(20); // 定義普通數據類型
</script>
reactive 基本使用
<template>
<div>
<div>{{wApper.subText}}</div>
<div v-for="item in list" :key="item.id">{{item.content}}</div>
</div>
</template>
<script setup>
const wapper = reactive({
subText: inner
});
const list = reactive([
{
id: 1,
content: "render"
},
{
id: 2,
content: "render2"
}
]);
</script>
當然你還可以配合 computed od watchEffec使用 這里我就不再過多介紹了。
1.2 ref 和 reactive 的區別?
相信大家讀到這里可以看出 ref 既可以定義基本數據類型 也可以 定義復雜數據類型,而reactive只定義復雜數據類型。
那有人就問了 ? reactive 只能存 復雜數據類型嗎?
答案很明顯不是的 reactive也可以存基本數據類型
那他們到底區別在哪里呢? 我想這個時候 從我們開發者的角度上沒辦法看出本質的區別,無非是定義變量唄,那接下來請隨者我一起進入源碼的是世界。
1.3 源碼實現流程 ?
1.3.1 如何找到源碼?
先回答第一個問題,怎么找源碼,這個需要你對源碼的包非常熟悉 我們可以通過看package.json文件先找到它打包的入口文件,然后再去根據不同的情況找不同的文件。
1.3.2 : 找到ref函數的源碼文件 ,看看函數內部做了什么事情?
源碼文件 : corepackagesreactivitysrcref.ts
ref.ts
核心代碼實現
// ref.ts 文件93 行
export function ref(value?: unknown) {
return createRef(value, false) //1 : 提供 ref函數 , false 是否淺復制
}
// ref.ts文件第 127行
// 調用 ref 返回一個 創建 的方法 createRef 傳入 兩個值
/**
* @param rawValue ref函數傳入的參數
* @param shallow 是否淺復制
*/
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) { // 是否是ref對象 如果是 則 直接返回
return rawValue
}
return new RefImpl(rawValue, shallow) // 否則 創建 ref 對象 傳入 rawValue shallow
}
// ref.ts 文件第 134行
class RefImpl<T> { // 創建一個 ref的 實現類
private _value: T // 創建私有的 _value 變量
private _rawValue: T // 創建私有的 _rawValue 變量
public dep?: Dep = undefined // 是否 dep
public readonly __v_isRef = true // 只讀的 屬性 是否是 ref
constructor(value: T, public readonly __v_isShallow: boolean) {
// 實例被 new時 執行 constructor 保存 傳入的值
this._rawValue = __v_isShallow ? value : toRaw(value) // 是否淺復制 , 如果時 則直接返回 傳入的值 否則進行 獲取其原始對象
this._value = __v_isShallow ? value : toReactive(value) // 是否淺復制 是 返回原value 否則 轉換成 reactive 對象
}
get value() { // 獲取值的時候 直接將 constructor 保存的值 返回
trackRefValue(this) // 跟蹤 ref 的 value
return this._value // 獲取value 是 返回 _value 對象
}
set value(newVal) {// 當 設置值的時候 往下看
// 是否淺復制 or 值身上是否有 __v_isShallow 標識 or 是否是只讀的 標識__v_isReadonly
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
// 如果滿足 則 返回新設置的值 , 如果不是 則 取出 新值的原始對象
newVal = useDirectValue ? newVal : toRaw(newVal) // 如果 你一個 淺層對象(普通數據類型) 則 原值返回 否則 判斷是否能從 代理對象中 取出源值
if (hasChanged(newVal, this._rawValue)) { // 判斷對象是否發生 變化 變了向下走
this._rawValue = newVal // 將最新值 賦給 _rawValue
this._value = useDirectValue ? newVal : toReactive(newVal) // 判斷是否是基本數據類型 如果 是 則 將最新值返回 否則 繼續轉換 reactive
triggerRefValue(this, newVal) // 觸發 ref 的 value 值進行監聽更新
}
}
}
// 判斷是否 是對象 如果是 則 reactive代理 否則 返回 當前的value
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
以上代碼就是 ref 的核心實現 , 相信看來好像源碼也沒有那么難。
1.3.3.總結一下 ref做了什么?
- 調用ref將 定義的數據傳入,返回一個創建ref響應式數據的函數,是否需要淺層復制,默認為false,也就意味著一定會走 toReactive
- 調用createRef, 判斷是否 是一個 ref對象 ,是 原值返回 否則 , new 一個 實現ref類
- 創建類的私有變量 ,保存傳入的value 和 shallow
- 判斷是否淺層復制,如果是則 返回傳入的 value,否則取出 ref的原始值對象
- 獲取值的時候將 保存的值 返回 出去
- 設置值的時候 判斷當前屬性 是否是淺層對象 ,如果是 則返回該數據 否則 調用 toreactive轉換 reactive 進行操作,如果是 普通數據類型,原值返回,按照 defineProperty 進行處理
- 觸發更新
1.3.4 : 找到reactve函數的源碼文件 ,看看函數內部做了什么事情?
reactive.ts
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) { // 如果是 只讀的 不允許 寫入 則返回只讀對象
return target
}
return createReactiveObject( // 返回一個創建 reactive 的對象
target, // 傳入 目標對象
false, // 是否是只讀對象
mutableHandlers, //提供 get, set, deleteProperty, has, ownKeys 方法
mutableCollectionHandlers, // 太多了 自己看源碼
reactiveMap // 提供一個 weakmap 集合
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) { // 如果不是一個對象 則 返回當前 traget
if (__DEV__) {
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
if (
target[ReactiveFlags.RAW] && // 如果target 已經是一個 代理對象 則 返回當前對象
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target) // 如果對象已經有了代理對象 則直接取值 返回
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target) // 觀察指定類型
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy( // 將對象進行代理
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy) // 設置目標為代理對象
return proxy // 將對象返回出去
}
1.3.5.總結一下 reactive做了什么?
- 調用reactive方法 傳數據,判斷 對象是否是只讀對象 如果是 直接返回 無法操作 否則向下繼續執行
- 調用 createReactiveObject 返回一個代理對象
- 判斷傳入的值是不是一個對象 如果不是則直接原路返回,否則判斷target 是不是一個 已經代理過了的對象
- 如果是代理過的對象 原路返回 否則 判斷目標對象是否有相應代理有直接取出響應代理對象 否則 繼續向下
- 針對不同的 值類型處理
- 代理整個trarget 我,將當前代理的對象設置到weakmap 中 將代理完的對象返回
1.4 總結
從源碼的角度來說 ref本身 會判斷是否為 基本數據類型 如果是 則是defineProperty 實現的如果是復雜數據類型就會按照 reactive進行處理 ,針對不同的數據類型會進行操作,當你ref為 對象時會轉換 reactive對象 將代理的對象 返回給 _value對象,如果是基本數據則會判斷是否需要淺層復制,不需要則直接返回了。 而 reactive 這邊也會判斷 是不是 基本數據類型 是 直接返回 否則就直接將對象進行了代理并返回。相信本篇文章能夠給你帶來一些啟發。
作者:前端小張同學
鏈接:
https://juejin.cn/post/7263411272892825655