嘿,日安!

Day 27 - toRef、toRefs、ProxyRef、unref

6th October 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:9th October 2025
5 Minutes
951 Words

響應式系統之中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: 18
10
})
11
12
const { name } = state
13
14
effect(() => {
15
console.log(name)
7 collapsed lines
16
})
17
18
setTimeout(() => {
19
state.name = 'b'
20
}, 1000)
21
</script>
22
</body>

執行這段程式碼,你會發現解構出來的屬性會遺失響應式,所以 setTimeout 不會觸發更新。

default

為了解決上述問題,我們通常會用 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: 18
10
})
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>

default

核心原理

如果這時候去看這個 name 輸出的類型

default

你會發現他跟我們在使用的 RefImpl 類型不同,它是一個特製的 ObjectRefImpl類別,並多了兩個屬性_object_key,它們分別儲存了原始物件、屬性名稱。

這個toRef我們可以知道他接受一個物件以及 key,所以我們可以這樣寫:

ref.ts
1
export function toRef(target, key) {
2
return{
3
get value() {
4
return target[key]
5
},
6
set value(newValue) {
7
target[key] = newValue
8
}
9
}
10
}

這樣子其實就可以更新,但官方範例是屬於個類別,所以我們也改寫成類別:

1
class ObjectRefImpl {
2
[ReactiveFlags.IS_REF] = true
3
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] = newValue
11
}
12
}
13
14
export 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: 18
10
})
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>

default

輸出age之後,可以看到它也是ObjectRefImpl類別。

default

那我們可以知道toRefs 的實現非常直觀,它遍歷目標物件的所有 key,並為每一個 key 呼叫 toRef

1
export function toRefs(target) {
2
const res = {}
3
for (const key in target) {
4
res[key] = new ObjectRefImpl(target, key)
5
}
6
return res
7
}

ps.toRefs原始碼中有另外寫判斷邏輯,確認傳入是不是響應式物件,這邊我們就省略判斷,讓它可以觸發更新:

default

雖然 toRefs 解決了響應性遺失的問題,但到處都是 .value,所以我們這邊需要兩個輔助工具。

unref

unref 是一個簡單的輔助函式,如果參數是 ref,它返回 .value;如果不是,則直接返回參數本身 。

1
export function unref(value) {
2
return isRef(value) ? value.value : value
3
}

ProxyRef

proxyRefs 可以將一個包含 ref 的物件(例如 toRefs 的回傳值)轉換為一個特殊的代理。當存取這個代理的屬性時,它會解包成ref。它跟 reactive 很像,不直接用 reactive 是因為 reactive 是深層物件,而 proxyRef 是淺層的物件。

1
export 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 來建立一個自動解包的代理物件。
Article title:Day 27 - toRef、toRefs、ProxyRef、unref
Article author:日安
Release time:6th October 2025