owned this note
owned this note
Published
Linked with GitHub
# VUE reactive() vs. ref() 都幾?
先說結論
reactive() 能做的事 ref() 也都能做
ref() 能做的事 reactive() 也都能做
真的要說差異的話只是 coding 風格的不同
## 使用上的差異
這裡簡單列出最顯而易見的差異
ref() 定義出來的值在 <script/> 裡需要 .value 才取得到 但在 <template/> 裡不用
而 reactive() 在 <script/> 還是 <template/> 裡都不用
ref() 可以允許 primitive values (原始值, 如 string, number, boolean, undefined) 以及 Objects (包括任何種類的 Object 和 Array)
而 reactive() 只允許 Objects
---
在探討哪個比較好這個問題之前
我們先來看看 ref() 和 reactive() 背後真正做了些什麼事
## [先看 ref()](https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts)
```typescript
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
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)
}
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 (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
```
由上面的 code 可以看出 ref.value 在 pass by reference (Objects) 的情況下實際上也是回傳 reactive() 的值
在 pass by value (primitive values) 的情況下則是直接回傳值
## [再看 reactive()](https://github.com/vuejs/core/blob/main/packages/reactivity/src/reactive.ts)
```typescript
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
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] &&
!(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
}
```
上面可以看出 最終 reactive() 會用傳入的值和 handlers 包出 [Proxy 物件](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
handlers 最終會在對這個 Proxy 進行 set, get... 等動作時 trigger effects
關於 Proxy 是如何真的使資料響應的 不在本文的範疇內 想了解的我推薦閱讀[這篇文章](https://vue-js.com/topic/61403f1861c8f900316ae580)
## 所以到底該用哪一個呢?
沒有一定要用哪一種 因為都能達到一樣的目的
但是可以總結成一句話:
> ### 如果將來這份資料需要被 re-assign 那就用 ref(), 反之用 reactive()
當然這只是我自己的看法 全都用 ref() 定義的人也大有人在
### 我不推薦使用 ref() 定義響應式資料
ref() 是允許值被 re-assign 的 但我認為這種操作對響應式資料是不好的
下面有一段程式碼每秒都會在 console 印出 "john one year older"
```typescript
const john = ref({
name: "John",
age: 30,
});
useIntervalFn(() => {
john.value.age++;
}, 1000);
watch(john.value, () => {
console.log("john one year older");
});
```
如果在這時候我多加這一段
```typescript
function Resurrection() {
john.value = {
name: "New John",
age: 0,
};
}
```
當 Resurrection() 被呼叫的時候 因為 watch 是基於 reference 來監聽的
在 re-assign 這個操作時 reference 會被換成新的 導致再也無法監聽到 john (但是 john 年齡仍在每秒增加)
當然你可以只 watch john 並把 deep 設為 true, 這樣也可以監聽到, 但我覺得不優雅
當響應式資料為 Object 或 Array 時 我推薦使用 reactive() 和 const 來定義, 這會強迫工程師養成不 re-assign 響應式資料的習慣, 可以讓 vue 響應式資料莫名其妙解綁的問題少一點
Object 可以用 Object.assign(), Array 可以用 array.splice()
### 那如果是實值 (pass by value) 的話勒
我會特別把他包成 Object 以方便能 reactive()
假設原本有一個 isShow: boolean 的響應式資料
```typescript
const isShow = ref(false);
```
用 reactive 會寫成
```typescript
const errorBox = reactive({
isShow: false,
});
```
會這麼做的原因是絕大多數的響應式資料如果用基本型別定義的話 通常都是只作用在這個 vue component 的一小區塊 包成 Object 會在命名的時候給更多資訊, 之後再看通常會比較快看懂, 尤其是當某個 vue component 有一大堆由基本型別定義的資料時, 這個差距會更明顯
當然這只是我自己的習慣 我認為這個情況用 ref() 也是完全能被接受的
### 蛤? 那什麼時候用 ref?
真的有時候需要 re-assign 值也是會用 ref() 啦 他沒有那麼十惡不赦, 只是我不愛用而已 :P
## 後記
### 一些 ref() 派的論點
> * Accessing variables with .value may irritate you, but I see that as an indicator that a variable is reactive.
> [source](https://medium.com/@bsalwiczek/ref-vs-reactive-in-vue-3-whats-the-right-choice-7c6f7265ce39)
靠 .value 當作識別響應式資料的做法也有 這算是不同的 coding style
但我個人認為被 .value 誤導的機率比使用 reactive 搞錯的機率大得多
畢竟 .value 算是一個蠻常見的屬性名字
> * 如果邏輯可以複用可以使用組合式函數,這樣其他組件也可以使用這個邏輯。reactive()函數返回的對象如果被解構的話,裏面的數據將會失去響應式,可以通過toRefs把對象裏面的每個屬性轉化成ref來使用。
> [source](https://www.readfog.com/a/1633537209551392768)
這個現象 ref() 也會發生 因為兩者都是套娃 Proxy 實作
只要是解構出來的東西不是 Proxy (當解構出來的值為原始值時) 就會失去響應
### 網路上一些容易誤解的說法
> * ref : 可以接受任何型態的資料,但是不會對物件或陣列內部的屬性變動做監聽。
> * reactive : 只能接受 Object 或 Array,會對內部的屬性變動做深層的監聽,取資料時不需要 .value。
> * 所以你可以從這邊去推論出 reactive 會去監控物件內的每一個屬性,而 ref 對於內部的改變並不會每一個屬性都去監控變化
> [source1](https://medium.com/i-am-mike/vue-3-ref-%E8%B7%9F-reactive-%E6%88%91%E8%A9%B2%E6%80%8E%E9%BA%BC%E9%81%B8-2fb6b6735a3c), [source2](https://campus-xoops.tn.edu.tw/modules/tad_book3/page.php?tbsn=33&tbdsn=1726)
當定義 Object 或 Array 的響應式資料時
無論是 ref 還是 reactive 都是套娃 Proxy 實作 都是深層的響應式資料
在使用 watch() 時 監聽不到物件或陣列內部的屬性變動 這是 ref.value 還是 reactive 都會有的問題 因為 watch 是淺層監聽 只會監聽物件的第一層