嘿,日安!

Day 28 - shallowRef、shallowReactive

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

refreactive 都屬於深層響應的 API 。它們會遞迴地將內部所有巢狀物件都轉換為響應式代理 。多數情況下非常方便,但當處理大型資料結構時,這種深度監聽的效能開銷可能會造成瓶頸。這時候我們就會使用到 shallowRefshallowReactive

ShallowRef

shallowRef 會建立一個 ref,但只對 .value 屬性本身的賦值操作具有響應性,它並不會將 .value 的內容轉換為響應式物件。

1
<body>
2
<div id="app"></div>
3
<script type="module">
4
import { shallowReactive, shallowRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
5
// import { reactive, effect, toRefs } from '../dist/reactivity.esm.js'
6
7
const c = shallowRef({
8
count: 1
9
});
10
11
effect(() => {
12
console.log(c.value.count)
13
})
14
15
setTimeout(() => {
4 collapsed lines
16
c.value.count++
17
}, 1000)
18
</script>
19
</body>

查看上述程式碼,你會觀察到effect只輸出一次,然而一秒鐘後setTimeout,並沒有輸出第二次。

如果希望輸出第二次的話,必須要把c.value.count++更改為c.value = { count: 2 }

default

default

依照慣例我們可以看到,console.log(c)的結果,他是屬於一個 RefImpl 類別的物件,但差異性是__v_isShallowtrue,由此我們可以知道它們是一樣的東西,但是透過這個標記來告訴類別說,我們現在這個物件是不是屬於淺層響應式物件。

1
// 如果 value 是物件,則使用 reactive 轉換為響應式物件
2
const convert = (value) => isObject(value) ? reactive(value) : value
3
4
class RefImpl implements Dependency{
5
_value;
6
[ReactiveFlags.IS_REF] = true
7
private _isShallow: boolean
8
9
subs: Link
10
subsTail: Link
11
constructor(value, isShallow) {
12
// 如果 isShallow 是 true,代表屬於淺層物件,直接回傳
13
this._value = isShallow ? value : convert(value)
14
}
15
16 collapsed lines
16
get value() {
17
if (activeSub) {
18
trackRef(this)
19
}
20
return this._value
21
}
22
23
set value(newValue) {
24
// 如果新值和舊值發生過變化,則更新
25
if(hasChanged(newValue, this._value)){
26
// 如果 isShallow 是 true,代表屬於淺層物件,直接回傳
27
this._value = this._isShallow ? newValue : convert(newValue)
28
triggerRef(this)
29
}
30
}
31
}

所以我們的實現思路是這樣:

  • RefImpl 類別增加一個 private_isShallow 屬性
  • 將「檢查值是否為物件,若是則用reactive 轉換」的邏輯封裝成 convert 函式 。
  • constructorsetter 中,根據 _isShallow 標記來決定是直接賦值,還是要經過 convert 函式處理 。

ShallowReactive

shallowReactive 只對物件的第一層屬性進行代理,任何對巢狀物件的修改都不會觸發響應 。

1
<body>
2
<div id="app"></div>
3
<script type="module">
4
import { shallowReactive, shallowRef, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
5
// import { reactive, effect, toRefs } from '../dist/reactivity.esm.js'
6
7
const state = shallowReactive({
8
a:1,
9
b:{
10
c:1
11
}
12
})
13
14
effect(() => {
15
console.log(state.b.c)
7 collapsed lines
16
})
17
18
setTimeout(() => {
19
state.b.c++
20
}, 1000)
21
</script>
22
</body>

shallowReactive 也是一樣的效果,state.b.c++沒有反應,需要變更為state.b = { c: 2 } }effect才會觸發輸出。

reactive 是通過mutableHandlers來處理,因此我們需要先重構這個mutableHandlers

baseHandlers.ts
1
const set = (target, key, newValue, receiver) => {
2
...
3
...
4
}
5
6
const get = (target, key, receiver) => {
7
// 收集依賴:綁定target的屬性與effect的關係
8
track(target, key)
9
const res = Reflect.get(target, key,receiver)
10
// 如果 res 是一個 ref,則返回 res.value
11
if(isRef(res)){
12
// target = {a:ref(0)}
13
return res.value
14
}
14 collapsed lines
15
16
if(isObject(res)){
17
/**
18
* 如果 res 是物件,則將其轉換為響應式物件
19
*/
20
return reactive(res)
21
}
22
return res
23
}
24
25
export const mutableHandlers = {
26
get,
27
set
28
}

先將 gettersetter 抽出來,再來調整 getter

1
function createGetter(isShallow = false) {
2
return function get(target, key, receiver) {
3
track(target, key)
4
const res = Reflect.get(target, key, receiver)
5
if (isRef(res)) {
6
return res.value
7
}
8
9
if (isObject(res)) {
10
// 關鍵:如果是 shallow,直接回傳原始物件,不再遞迴呼叫 reactive
11
return isShallow ? res : reactive(res)
12
}
13
return res
14
}
15
}
2 collapsed lines
16
17
const get = createGetter()

我們將get 的邏輯抽離成一個工廠函式 createGetter,並用 isShallow 參數來控制是否要遞迴地將巢狀物件轉換為 reactive

1
const shallowGet = createGetter(true)
2
3
export const shallowReactiveHandlers = {
4
get: shallowGet,
5
set
6
}

建立不同 handler 並導出,之後我們在reactive.ts 引入

reactive.ts
1
import { mutableHandlers, shallowReactiveHandlers } from './baseHandlers'
2
3
function createReactiveObject(target, handlers, proxyMap) {
4
5
if (!isObject(target)) return target
6
7
// 如果這個 target 儲存在 reactiveSet 中
8
// 表示 target 是一個響應式物件,直接返回已經建立好的 proxy
9
if(reactiveSet.has(target)){
10
return proxyMap.get(target)
11
}
12
13
// 如果這個 target 已經被 reactive 過了,直接返回已經建立好的 proxy
14
const existingProxy = proxyMap.get(target)
14 collapsed lines
15
if (existingProxy) {
16
return existingProxy
17
}
18
19
// 根據傳入參數 handler,建立 target 的代理物件
20
const proxy = new Proxy(target, handlers)
21
22
// 儲存 target 和響應式物件的關聯關係
23
proxyMap.set(target, proxy)
24
25
reactiveSet.add(proxy)
26
27
return proxy
28
}

調整createReactiveObject函式,讓它可以根據 handler 以及 proxyMap 來決定是不是要建立深層響應式物件。

reactive.ts
1
// 新增 shallowReactive 的快取
2
const shallowReactiveMap = new WeakMap()
3
4
export function reactive(target) {
5
return createReactiveObject(target, mutableHandlers, reactiveMap)
6
}
7
8
// 新增 shallowReactive 函式
9
export function shallowReactive(target) {
10
return createReactiveObject(target, shallowReactiveHandlers, shallowReactiveMap)
11
}

這樣我們就完成了 shallowReactive,現在shallowReactive以及shallowRef都已經完成。

「深層響應」為帶來了開發上的便利,而「淺層響應」則讓我們有更近一步優化效能的機會。在一般開發過程中,我們應該優先使用 refreactive,當遇到明確的效能瓶頸時,可以觀察我們自己在使用資料的情況,適度替換成shallow 版本。

Article title:Day 28 - shallowRef、shallowReactive
Article author:日安
Release time:7th October 2025