響應式 API 之 toRef 與 toRefs
前面講了 ref 和 reactive 這兩種響應式API ,為了方便開發者使用,vue3 還出了兩個用來 reactive 轉換為 ref 的API,分別是 toRef 和 toRefs 。
🌈什么是toRef 與 toRefs
這兩個API看拼寫能猜到,toRef
轉換一個ref,toRefs
是轉換所有 ref 。轉換后將得到新的變量,并且新變量和原來的變量可以保持同步更新。
API | 作用 |
---|---|
toRef | 創建一個新的 Ref 變量,轉換 Reactive 對象的某個字段為 Ref 變量 |
toRefs | 創建一個新的對象,它的每個字段都是 Reactive 對象各個字段的 Ref 變量 |
光看概念可能不容易理解,來看下面的例子,先聲明一個 reactive
變量:
interface Member {id: numbername: string
}
const userInfo : Member = reactive({id: 1 ,name: "張珊珊"
})
🌈為什么要進行轉換
為什么要出這么兩個 API ,官方文檔沒有特別說明,不過經過筆者在業務中的一些實際使用感受,可知道一些使用理由。
ref
和 reactive
在使用的過程中,各自都有不方便的地方:
ref
API 雖然在 <template />
里使用起來方便,但是在 <script />
里進行讀取 / 賦值的時候,要一直記得加上 .value
,否則 BUG 就來了。
reactive
API 雖然在使用的時候,因為知道它本身是一個對象,所以不會忘記通過 foo.bar
這樣的格式去操作,但是在 <template />
渲染的時候,又因此不得不每次都使用 foo.bar
的格式去渲染。
那么有沒有辦法,既可以在編寫 <script />
的時候不容易出錯,在寫 <template />
的時候又比較簡單呢?
于是, toRef
和 toRefs
因此誕生。
🌈toRef的使用
🚀API的TS類型和基本用法
// `toRef` API 的 TS 類型
function toRef<T extends object, K extends keyof T>(object: T,key: K,defaultValue?: T[K]
): ToRef<T[K]>// `toRef` API 的返回值的 TS 類型
type ToRef<T> = T extends Ref ? T : Ref<T>
通過接收兩個必傳的參數(第一個是 reactive
對象, 第二個是要轉換的 key
),返回一個 Ref 變量,在適當的時候也可以傳遞第三個參數,為該變量設置默認值。
以上面聲明好的 userInfo 為例子,如果想要轉換 name 字段為 Ref 變量,需要:
const name = toRef(userInfo , 'name')
console.log(name.value)
等號左側的 name
變量此時是一個 Ref 變量,這里因為 TypeScript 可以對其自動推導,因此聲明時可以省略 TS 類型的顯式指定,實際上該變量的類型是 Ref<string>
。
所以之后在讀取和賦值時,就需要使用 name.value
來操作,在重新賦值時會同時更新 name
和 userInfo.name
的值:
// 修改前先查看初始值
const name = toRef(userInfo, 'name')
console.log(name.value) // Petter
console.log(userInfo.name) // Petter// 修改 Ref 變量的值,兩者同步更新
name.value = 'Tom'
console.log(name.value) // Tom
console.log(userInfo.name) // Tomty// 修改 Reactive 對象上該屬性的值,兩者也是同步更新
userInfo.name = 'Jerry'
console.log(name.value) // Jerry
console.log(userInfo.name) // Jerry
這個 API 也可以接收一個 Reactive 數組,此時第二個參數應該傳入數組的下標:
// 這一次聲明的是數組
const words = reactive(['a', 'b', 'c'])// 通過下標 `0` 轉換第一個 item
const a = toRef(words, 0)
console.log(a.value) // a
console.log(words[0]) // a// 通過下標 `2` 轉換第三個 item
const c = toRef(words, 2)
console.log(c.value) // c
console.log(words[2]) // c
🚀基本用法-完整代碼和演示
<script lang="ts">
import { defineComponent, toRef, reactive, toRefs } from "vue"
interface Member {id: numbername: string
}
const userInfo: Member = reactive({id: 1,name: "張珊珊"
})export default defineComponent({setup() {// 在這里聲明數據,或者編寫函數并在這里執行它const name = toRef(userInfo, 'name');function editRefName() {name.value = '陳圓圓'}function editReactiveName() {userInfo.name = '裴南葦'}return {name,userInfo,editRefName,editReactiveName,}},
})</script><template><div><h2> ref: {{ name }}</h2><h2> reactive: {{ userInfo.name }}</h2><button @click="editRefName()">修改ref的name</button><br><button @click="editReactiveName()">修改reactive 的name</button></div>
</template><style scoped></style>
代碼分別顯示了 ref 和 reactive 的兩種響應式的值的顯示。又分別控制了,修改兩個值。下面的演示結果可以看出,toRef
轉換后將得到新的變量,并且新變量和原來的變量可以保持同步更新。
演示動圖
🚀設置默認值
如果 Reactive 對象上有一個屬性本身沒有初始值,也可以傳遞第三個參數進行設置(默認值僅對 Ref 變量有效,即此次不會同步):
interface Member {id: numbername: stringage?: number
}const userInfo: Member = reactive({id: 2,name: "Tony"
})// 此時為了避免程序運行錯誤,可以指定一個初始值
// 但初始值僅對 Ref 變量有效,不會影響 Reactive 字段的值
const age = toRef( userInfo , age , 18)
console.log(age.value) // 18
console.log(userInfo.age) // undefined// 除非重新賦值,才會使兩者同時更新
age.value = 25
console.log(age.value) // 25
console.log(userInfo.age) // 25
數組也是同理,對于可能不存在的下標,可以傳入默認值避免項目的邏輯代碼出現問題:
const words = reactive(['a','b','c'])
//當下標對應的值不存在時,返回 ”undefined“
const d = toRef(words,3)
console.log(d.value) // undefined
console.log(words[3]) // undefined//設置了默認后,會僅對 Ref 變量有效
cosnt e = toRef(words,4,'e')
console.log(e.value) // e
console.log(words[4]) // undefined
🚀其他用法
這個 API 還有一個特殊用法,但不建議在 TypeScript 里使用。
在 toRef
的過程中,如果使用了原對象上面不存在的 key
,那么定義出來的 Ref 變量的 .value
值將會是 undefined
。
// 眾所周知, Petter 是沒有女朋友的
const girlfriend = toRef(userInfo, 'girlfriend')
console.log(girlfriend.value) // undefined
console.log(userInfo.girlfriend) // undefined// 此時 Reactive 對象上只有兩個 Key
console.log(Object.keys(userInfo)) // ['id', 'name']
如果對這個不存在的 key
的 Ref 變量進行賦值,那么原來的 Reactive 對象也會同步增加這個 key
,其值也會同步更新。
// 賦值后,不僅 Ref 變量得到了 `Marry` , Reactive 對象也得到了 `Marry`
girlfriend.value = 'Marry'
console.log(girlfriend.value) // 'Marry'
console.log(userInfo.girlfriend) // 'Marry'// 此時 Reactive 對象上有了三個 Key
console.log(Object.keys(userInfo)) // ['id', 'name', 'girlfriend']
為什么強調不要在 TypeScript 里使用呢?因為在編譯時,無法通過 TypeScript 的類型檢查:
? npm run build> hello-vue3@0.0.0 build
> vue-tsc --noEmit && vite buildsrc/views/home.vue:37:40 - error TS2345: Argument of type '"girlfriend"'
is not assignable to parameter of type 'keyof Member'.37 const girlfriend = toRef(userInfo, 'girlfriend')~~~~~~~~~~~~src/views/home.vue:39:26 - error TS2339: Property 'girlfriend' does not exist
on type 'Member'.39 console.log(userInfo.girlfriend) // undefined~~~~~~~~~~src/views/home.vue:45:26 - error TS2339: Property 'girlfriend' does not exist
on type 'Member'.45 console.log(userInfo.girlfriend) // 'Marry'~~~~~~~~~~Found 3 errors in the same file, starting at: src/views/home.vue:37
如果不得不使用這種情況,可以考慮使用 any 類型:
// 將該類型直接指定為 `any`
type Member = any
// 當然一般都是 `const userInfo: any`// 或者保持接口類型的情況下,允許任意鍵值
interface Member {[key: string]: any
}// 使用 `Record` 也是同理
type Member = Record<string, any>
但筆者還是更推薦保持良好的類型聲明習慣,盡量避免這種用法。
🌈使用 toRefs
在了解了 toRef
API 之后,來看看 toRefs
的用法。
🚀API 類型和基本用法
先看看它的 TS 類型:
function toRefs<T extends object>(object: T
): {[K in keyof T]: ToRef<T[K]>
}type ToRef = T extends Ref ? T : Ref<T>
與 toRef 不同, toRefs 只接收了一個參數:reactive 變量。
interface Member {id: numbername: string
}// 聲明一個 Reactive 變量
const userInfo: Member = reactive({id: 1,name: 'Petter',
})// 傳給 `toRefs` 作為入參
const userInfoRefs = toRefs(userInfo)
此時這個新的 userInfoRefs
變量,它的 TS 類型就不再是 Member
了,而應該是:
// 導入 `toRefs` API 的類型
import type { ToRefs } from 'vue'// 上下文代碼省略...// 將原來的類型傳給 API 的類型
const userInfoRefs: ToRefs<Member> = toRefs(userInfo)
也可以重新編寫一個新的類型來指定它,因為每個字段都是與原來關聯的 Ref 變量,所以也可以這樣聲明:
// 導入 `ref` API 的類型
import type { Ref } from 'vue'// 上下文代碼省略...// 新聲明的類型每個字段都是一個 Ref 變量的類型
interface MemberRefs {id: Ref<number>name: Ref<string>
}// 使用新的類型進行聲明
const userInfoRefs: MemberRefs = toRefs(userInfo)
當然實際上日常使用時并不需要手動指定其類型, TypeScript 會自動推導,可以節約非常多的開發工作量。
和 toRef
API 一樣,這個 API 也是可以對數組進行轉換:
const words = reactive(['a', 'b', 'c'])
const wordsRefs = toRefs(words)
此時新數組的類型是 Ref<string>[]
,不再是原來的 string[]
類型。
🚀解構與賦值
轉換后的 Reactive 對象或數組支持 ES6 的解構,并且不會失去響應性,因為解構后的每一個變量都具備響應性。
// 為了提高開發效率,可以直接將 Ref 變量直接解構出來使用
const { name } = toRefs(userInfo)
console.log(name.value) // Petter// 此時對解構出來的變量重新賦值,原來的變量也可以同步更新
name.value = 'Tom'
console.log(name.value) // Tom
console.log(userInfo.name) // Tom
這一點和直接解構 Reactive 變量有非常大的不同,直接解構 Reactive 變量,得到的是一個普通的變量,不再具備響應性。
這個功能在使用 Hooks 函數非常好用(在 Vue 3 里也叫可組合函數, Composable Functions ),還是以一個計算器函數為例,這一次將其修改為內部有一個 Reactive 的數據狀態中心,在函數返回時解構為多個 Ref 變量:
import { reactive, toRefs } from 'vue'// 聲明 `useCalculator` 數據狀態類型
interface CalculatorState {// 這是要用來計算操作的數據num: number// 這是每次計算時要增加的幅度step: number
}// 聲明一個 “使用計算器” 的函數
function useCalculator() {// 通過數據狀態中心的形式,集中管理內部變量const state: CalculatorState = reactive({num: 0,step: 10,})// 功能函數也是通過數據中心變量去調用function add() {state.num += state.step}return {...toRefs(state),add,}
}
這樣在調用 useCalculator
函數時,可以通過解構直接獲取到 Ref 變量,不需要再進行額外的轉換工作。
// 解構出來的 `num` 和 `step` 都是 Ref 變量
const { num, step, add } = useCalculator()
console.log(num.value) // 0
console.log(step.value) // 10// 調用計算器的方法,數據也是會得到響應式更新
add()
console.log(num.value) // 10
🌈什么場景下比較適合使用它們
從便利性和可維護性來說,最好只在功能單一、代碼量少的組件里使用,比如一個表單組件,通常表單的數據都放在一個對象里。
當然也可以把所有的數據都定義到一個 data
里,再去 data
里面取值,但是沒有必要為了轉換而轉換,否則不如使用 Options API 風格。
🌈在業務中的具體運用
繼續使用上文一直在使用的 userInfo
來當案例,以一個用戶信息表的小 demo 做個演示。
在 <script />
部分:
- 先用
reactive
定義一個源數據,所有的數據更新,都是修改這個對象對應的值,按照對象的寫法維護數據 - 再通過
toRefs
定義一個給<template />
使用的對象,這樣可以得到一個每個字段都是 Ref 變量的新對象 - 在
return
的時候,對步驟 2 里的toRefs
對象進行解構,這樣導出去就是各個字段對應的 Ref 變量,而不是一整個對象
import { defineComponent, reactive, toRefs } from 'vue'interface Member {id: numbername: stringage: numbergender: string
}export default defineComponent({setup() {// 定義一個 reactive 對象const userInfo = reactive({id: 1,name: 'Petter',age: 18,gender: 'male',})// 定義一個新的對象,它本身不具備響應性,但是它的字段全部是 Ref 變量const userInfoRefs = toRefs(userInfo)// 在 2s 后更新 `userInfo`setTimeout(() => {userInfo.id = 2userInfo.name = 'Tom'userInfo.age = 20}, 2000)// 在這里解構 `toRefs` 對象才能繼續保持響應性return {...userInfoRefs,}},
})
在 <template />
部分:
由于 return
出來的都是 Ref 變量,所以在模板里可以直接使用 userInfo
各個字段的 key
,不再需要寫很長的 userInfo.name
了。
<template><ul class="user-info"><li class="item"><span class="key">ID:</span><span class="value">{{ id }}</span></li><li class="item"><span class="key">name:</span><span class="value">{{ name }}</span></li><li class="item"><span class="key">age:</span><span class="value">{{ age }}</span></li><li class="item"><span class="key">gender:</span><span class="value">{{ gender }}</span></li></ul>
</template>
🌈需要注意的問題
請注意是否有相同命名的變量存在,比如上面在 return
給 <template />
使用時,在解構 userInfoRefs
的時候已經包含了一個 name
字段,此時如果還有一個單獨的變量也叫 name
,就會出現渲染上的數據顯示問題。
此時它們在 <template />
里哪個會生效,取決于誰排在后面,因為 return
出去的其實是一個對象,在對象里,如果存在相同的 key
,則后面的會覆蓋前面的。
下面這種情況,會以單獨的 name
為渲染數據:
return {...userInfoRefs,name,
}
而下面這種情況,則是以 userInfoRefs
里的 name
為渲染數據:
return {name,...userInfoRefs,
}
所以當決定使用 toRef
和 toRefs
API 的時候,請注意這個特殊情況!