響應式系統之中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
來建立一個自動解包的代理物件。