響應式系統之中reactive 能夠將一個物件轉換為深層的響應式物件,但是在開發過程中我們時常會需要用到解構賦值,這時候會導致響應性遺失。
問題解析
1<body>2 <div id="app"></div>3 <script type="module">4 import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'5 // import { reactive, effect, ref } from '../dist/reactivity.esm.js'6
7 const state = reactive({8 name: 'a',9 age: 1810 })11
12 const { name } = state13
14 effect(() => {15 console.log(name)7 collapsed lines
16 })17
18 setTimeout(() => {19 state.name = 'b'20 }, 1000)21 </script>22</body>執行這段程式碼,你會發現解構出來的屬性會遺失響應式,所以 setTimeout 不會觸發更新。

為了解決上述問題,我們通常會用 toRef ,讓解構出來的變數可以觸發響應式更新:
1<body>2 <div id="app"></div>3 <script type="module">4 import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'5 // import { reactive, effect, ref } from '../dist/reactivity.esm.js'6
7 const state = reactive({8 name: 'a',9 age: 1810 })11
12 const name = toRef(state, 'name')13
14 effect(() => {15 console.log(name.value)7 collapsed lines
16 })17
18 setTimeout(() => {19 state.name = 'b'20 }, 1000)21 </script>22</body>
核心原理
如果這時候去看這個 name 輸出的類型

你會發現他跟我們在使用的 RefImpl 類型不同,它是一個特製的 ObjectRefImpl類別,並多了兩個屬性_object、_key,它們分別儲存了原始物件、屬性名稱。
這個toRef我們可以知道他接受一個物件以及 key,所以我們可以這樣寫:
1export function toRef(target, key) {2 return{3 get value() {4 return target[key]5 },6 set value(newValue) {7 target[key] = newValue8 }9 }10}這樣子其實就可以更新,但官方範例是屬於個類別,所以我們也改寫成類別:
1class ObjectRefImpl {2 [ReactiveFlags.IS_REF] = true3 constructor(public _object, public key) {}4
5 get value() {6 return this._object[this.key]7 }8
9 set value(newValue) {10 this._object[this.key] = newValue11 }12}13
14export function toRef(target, key) {15 return new ObjectRefImpl(target, key)1 collapsed line
16}這樣可以將我們解構出來的變數,重新賦予響應性。
toRefs
當需要處理多個屬性時,可以使用 toRefs,它會遍歷一個reactive 物件,並將其所有屬性都轉換為 ref,使用如下:
1<body>2 <div id="app"></div>3 <script type="module">4 import { reactive, toRefs, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'5 // import { reactive, effect, toRef } from '../dist/reactivity.esm.js'6
7 const state = reactive({8 name: 'a',9 age: 1810 })11 const {name, age} = toRefs(state)12
13 effect(() => {14 console.log(age.value)15 })6 collapsed lines
16
17 setTimeout(() => {18 state.age++19 }, 1000)20 </script>21</body>
輸出age之後,可以看到它也是ObjectRefImpl類別。

那我們可以知道toRefs 的實現非常直觀,它遍歷目標物件的所有 key,並為每一個 key 呼叫 toRef :
1export function toRefs(target) {2 const res = {}3 for (const key in target) {4 res[key] = new ObjectRefImpl(target, key)5 }6 return res7}ps.toRefs原始碼中有另外寫判斷邏輯,確認傳入是不是響應式物件,這邊我們就省略判斷,讓它可以觸發更新:

雖然 toRefs 解決了響應性遺失的問題,但到處都是 .value,所以我們這邊需要兩個輔助工具。
unref
unref 是一個簡單的輔助函式,如果參數是 ref,它返回 .value;如果不是,則直接返回參數本身 。
1export function unref(value) {2 return isRef(value) ? value.value : value3}ProxyRef
proxyRefs 可以將一個包含 ref 的物件(例如 toRefs 的回傳值)轉換為一個特殊的代理。當存取這個代理的屬性時,它會解包成ref。它跟 reactive 很像,不直接用 reactive 是因為 reactive 是深層物件,而 proxyRef 是淺層的物件。
1export function proxyRefs(target) {2 return new Proxy(target, {3 get(...args) {4 const res = Reflect.get(...args)5 return unref(res)6 },7 set(target, key, newValue, receiver) {8 return Reflect.set(target, key, newValue, receiver)9 }10 })11}這樣就完成了proxyRefs。
今天我們重點在於:
- 直接從
reactive物件中解構,會失去響應性,所以可以使用toRefs將整個物件的所有屬性轉換成 ref,再進行解構。 - 使用
toRefs將整個物件的所有屬性轉換成 ref,再進行解構。這樣每個被解構出來的變數都與原始物件進行了響應式連結。 - 選擇性地使用
proxyRefs來建立一個自動解包的代理物件。