響應式系統之中reactive
能夠將一個物件轉換為深層的響應式物件,但是在開發過程中我們時常會需要用到解構賦值,這時候會導致響應性遺失。
<body>
<div id="app"></div>
<script type="module">
import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { reactive, effect, ref } from '../dist/reactivity.esm.js'
const state = reactive({
name: 'a',
age: 18
})
const { name } = state
effect(() => {
console.log(name)
})
setTimeout(() => {
state.name = 'b'
}, 1000)
</script>
</body>
執行這段程式碼,你會發現解構出來的屬性會遺失響應式,所以 setTimeout
不會觸發更新。
為了解決上述問題,我們通常會用 toRef
,讓解構出來的變數可以觸發響應式更新:
<body>
<div id="app"></div>
<script type="module">
import { reactive, toRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { reactive, effect, ref } from '../dist/reactivity.esm.js'
const state = reactive({
name: 'a',
age: 18
})
const name = toRef(state, 'name')
effect(() => {
console.log(name.value)
})
setTimeout(() => {
state.name = 'b'
}, 1000)
</script>
</body>
如果這時候去看這個 name
輸出的類型
你會發現他跟我們在使用的 RefImpl
類型不同,它是一個特製的 ObjectRefImpl
類別,並多了兩個屬性_object
、_key
,它們分別儲存了原始物件、屬性名稱。
這個toRef
我們可以知道他接受一個物件以及 key,所以我們可以這樣寫:
//ref.ts
export function toRef(target, key) {
return{
get value() {
return target[key]
},
set value(newValue) {
target[key] = newValue
}
}
}
這樣子其實就可以更新,但官方範例是屬於個類別,所以我們也改寫成類別:
class ObjectRefImpl {
[ReactiveFlags.IS_REF] = true
constructor(public _object, public key) {}
get value() {
return this._object[this.key]
}
set value(newValue) {
this._object[this.key] = newValue
}
}
export function toRef(target, key) {
return new ObjectRefImpl(target, key)
}
這樣可以將我們解構出來的變數,重新賦予響應性。
當需要處理多個屬性時,可以使用 toRefs
,它會遍歷一個reactive
物件,並將其所有屬性都轉換為 ref
,使用如下:
<body>
<div id="app"></div>
<script type="module">
import { reactive, toRefs, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
// import { reactive, effect, toRef } from '../dist/reactivity.esm.js'
const state = reactive({
name: 'a',
age: 18
})
const {name, age} = toRefs(state)
effect(() => {
console.log(age.value)
})
setTimeout(() => {
state.age++
}, 1000)
</script>
</body>
輸出age
之後,可以看到它也是ObjectRefImpl
類別。
那我們可以知道toRefs
的實現非常直觀,它遍歷目標物件的所有 key
,並為每一個 key
呼叫 toRef
:
export function toRefs(target) {
const res = {}
for (const key in target) {
res[key] = new ObjectRefImpl(target, key)
}
return res
}
ps.toRefs
原始碼中有另外寫判斷邏輯,確認傳入是不是響應式物件,這邊我們就省略判斷,讓它可以觸發更新:
雖然 toRefs
解決了響應性遺失的問題,但到處都是 .value
,所以我們這邊需要兩個輔助工具。
unref
是一個簡單的輔助函式,如果參數是 ref
,它返回 .value
;如果不是,則直接返回參數本身 。
export function unref(value) {
return isRef(value) ? value.value : value
}
proxyRefs
可以將一個包含 ref
的物件(例如 toRefs
的回傳值)轉換為一個特殊的代理。當存取這個代理的屬性時,它會解包成ref
。它跟 reactive
很像,不直接用 reactive
是因為 reactive 是深層物件,而 proxyRef
是淺層的物件。
export function proxyRefs(target) {
return new Proxy(target, {
get(...args) {
const res = Reflect.get(...args)
return unref(res)
},
set(target, key, newValue, receiver) {
return Reflect.set(target, key, newValue, receiver)
}
})
}
這樣就完成了proxyRefs
。
今天我們重點在於:
reactive
物件中解構,會失去響應性,所以可以使用 toRefs
將整個物件的所有屬性轉換成 ref,再進行解構。toRefs
將整個物件的所有屬性轉換成 ref,再進行解構。這樣每個被解構出來的變數都與原始物件進行了響應式連結。proxyRefs
來建立一個自動解包的代理物件。同步更新《嘿,日安!》技術部落格