前言
Vue3有個(gè)Ref API,官網(wǎng)文檔 說明其主要的用處是:1剔交、將一個(gè)原始類型值 (例如肆饶,一個(gè)字符串),變成響應(yīng)式的岖常。2驯镊、當(dāng)解構(gòu)的兩個(gè) property 的響應(yīng)性都會(huì)丟失時(shí),可以將我們的響應(yīng)式對象轉(zhuǎn)換為一組 ref竭鞍。這些 ref 將保留與源對象的響應(yīng)式關(guān)聯(lián)板惑。
下面是對應(yīng)的兩個(gè)例子:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
import { reactive, toRefs } from 'vue'
const book = reactive({
author: 'Vue Team',
title: 'Vue 3 Guide',
})
let { author, title } = toRefs(book)
title.value = 'Vue 3 Detailed Guide' // 我們需要使用 .value 作為標(biāo)題,現(xiàn)在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'
由于 Proxy 只能代理對象偎快,那么Vue3是如何代理原始類型值的呢洒放?
Ref 源碼在 packages/reactivity/src/ref.ts 下,閱讀版本是v3.2.30
滨砍,ref.ts
只有200多行代碼往湿⊙欤看一下 Ref 是怎么實(shí)現(xiàn)的。
正文
(1)Ref
找到ref
函數(shù)领追,其調(diào)用路徑是 ref
-> createRef
-> new RefImpl
他膳,RefImpl
是一個(gè)類,其實(shí)現(xiàn)很簡單绒窑,提供一個(gè)value
屬性和value
的訪問器get
棕孙、set
。
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
也就是說些膨,ref
的實(shí)現(xiàn)蟀俊,還是使用對象,將原始類型值包一層订雾,存到對象的value屬性上肢预。這也是為什么需要用 count.value
來訪問和設(shè)置屬性值。最終還是要使用對象的訪問器get
來收集依賴(track)洼哎、set
來觸發(fā)更新(trigger)烫映。我們可以自己簡單實(shí)現(xiàn)一個(gè)ref:
打開 Vue SFC Playground: ref 可查看和運(yùn)行上面的例子。
(2)toRef噩峦、toRefs
接下來看看ref.ts
中toRef
锭沟、toRefs
的實(shí)現(xiàn),查看這兩個(gè)API的使用识补,發(fā)現(xiàn)其就是使用 ref 與源響應(yīng)式對象字段的響應(yīng)式關(guān)聯(lián)起來族淮。其解決的問題是,當(dāng)響應(yīng)式對象的屬性解構(gòu)到新的普通對象newObj
時(shí)凭涂,響應(yīng)式會(huì)丟失:
const book = reactive({
author: 'Vue Team',
title: 'Vue 3 Guide',
})
const newObj = {
...book
}
newObj.author // 收集不到依賴
newObj.author = "Team" // 觸發(fā)不了更新
下面看看toRef瞧筛、toRefs:
他們的實(shí)現(xiàn)很簡單,和實(shí)現(xiàn)ref的原理一樣导盅,也是包一層,提供一個(gè)value字段揍瑟。當(dāng)
get
或set
這個(gè)ref.value時(shí)白翻,操作的是源響應(yīng)式對象。達(dá)到與源響應(yīng)式對象字段的響應(yīng)式關(guān)聯(lián)的目的绢片,從而解決響應(yīng)式丟失問題滤馍。打開 Vue SFC Playground: toRef、toRefs 可查看自己實(shí)現(xiàn)的toRef底循、toRefs巢株。
(3)ref.value問題與proxyRefs
當(dāng)我們使用ref時(shí),很多人吐槽熙涤,我們總是要加一個(gè).value
阁苞,用起來太不爽了困檩。但我們發(fā)現(xiàn),我們在模板中使用ref的時(shí)候那槽,并不需要.value
悼沿,那它是怎么實(shí)現(xiàn)的呢?
在ref.ts
中還有一個(gè) proxyRefs
:
可以看到骚灸,它就是對包含 ref類型字段的對象再包一層糟趾,把普通對象newObj也轉(zhuǎn)成代理對象!當(dāng)訪問 newObj.foo
時(shí)甚牲,判斷newObj.foo
是不是一個(gè)ref义郑,如果是則代理到newObj.foo.value
。
然后我們搜一下proxyRefs
在哪里有使用丈钙,可以看到在packages/runtime-core/src/component.ts
中非驮,handleSetupResult
會(huì)將 setupResult
傳入 proxyRefs
!這就是我們模板中使用 ref 對象(newObj.foo
)不用加.value
的原因著恩。
至此院尔,模板中的使用問題解決了。但我們還有個(gè)問題是喉誊,在script中使用還是需要.value
:
function add() {
newObj.foo.value++
}
對于這個(gè)問題邀摆,尤大提了一個(gè)提案 Ref Sugar 的 RFC,即 ref
語法糖伍茄,目前還處理實(shí)驗(yàn)性的(Experimental)階段栋盹。提案的主要內(nèi)容是尤大想提供一個(gè)語法糖$ref
。使用$ref
敷矫,編譯器編譯時(shí)會(huì)自動(dòng)加上.value例获,這樣我們編寫代碼時(shí)就不用使用.value了:
import { $ref } from 'vue'
const count = $ref(0)
console.log(count) // 0
count++
console.log(count) // 1
我們通過 Vue Playground: $ref 可以直觀地感受一下。
總結(jié)
至此曹仗,ref的源碼也就看得差不多了榨汤,可以看到這是一個(gè)多重代理的東西。如果我們在模板中使用一個(gè)ref屬性(newObj.foo
)怎茫,看看其經(jīng)過的代理:
newObj.foo
-> newObj.foo.value
-> reactiveObj.foo
-> rawObj.foo
收壕。
其中rawObj是我們最初的普通對象。所以標(biāo)題說這真是套娃轨蛤。