嘿,日安!

Day 18 - Reactive:深入 Proxy 的設計思路

27th September 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:29th September 2025
7 Minutes
1348 Words

在之前的文章中,我們已經完成了 ref 實作,它能將原始值包裝成響應式物件。現在,我們要接續完成另一部分的響應式系統核心:reactive 函式。我們的目標是接收一個完整的物件,並回傳一個代理物件,使其所有屬性都具備響應性。

目標設定

我們的目標很明確:完成一個 reactive 函式,讓行為跟 Vue 的官方範例一樣。

環境建置

1
// import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
2
import { reactive, effect } from '../dist/reactivity.esm.js'
3
4
const state = reactive({
5
a: 0
6
})
7
effect(() => {
8
console.log(state.a)
9
})
10
11
setTimeout(() => {
12
state.a = 1
13
}, 1000)

我們期待初始化頁面輸出 0,一秒鐘後輸出1。

用註解的官方的範例,我們很明顯看到輸出值。

default

我們先在src底下新增一個 reactive.ts

1
export function reactive(target){
2
}

並且在 index.ts 引入

1
export * from './ref'
2
export * from './effect'
3
export * from './reactive'

另外我們在 shared/src/index.ts 存放工具函式這邊寫一個物件判斷函式。

1
export function isObject(value) {
2
return typeof value === 'object' && value !== null
3
}

核心思路

我們再另外寫一個函式createReactiveObject,我們實際的邏輯並不在 reactive函式中。

主要是createReactiveObject之後其他地方會用到,像是 shallowReactive 之類的。

1
export function reactive(target){
2
return createReactiveObject(target)
3
}

接下來思考 createReactiveObject他本身的限制,以及我們的需求,

  1. 他只能傳入物件類型,所以我們要去判斷他的型別。
  2. reactive 的核心是用一個 Proxy 物件來處理。
  3. Proxy 的物件中會需要 get 和 set 處理收集依賴、觸發更新。
  • 收集依賴:target 本身就是依賴,因此我們需要在收集依賴時,把 targeteffect(也就是sub)建立關聯關係。
  • 觸發更新:通知之前收集的依賴,重新執行。

為什麼 Vue 3 的 reactive() 特別適合使用 Proxy?

主要是因為有幾個特性

  • Proxy 可以攔截並自定義物件的各種操作,不只是屬性的讀取和設置
  • 與 Vue 2 使用 Object.defineProperty() 相比,Proxy 的最大優勢是可以偵測新增的屬性
  • Proxy 可以直接攔截陣列的索引操作和 length 變更
  • Proxy 可以處理 MapSetWeakMapWeakSet 等集合類型

看來針對物件類型的 reactiveProxy 物件的確是一個更好的解決方案,那我們開始實作!

初步實作 - 借鏡 Ref 實作

1
import { isObject } from '@vue/shared'
2
3
function createReactiveObject(target){
4
// reactive 只處理物件
5
if(!isObject(target)) return target
6
7
// 建立 target 的代理物件
8
const proxy = new Proxy(target, {
9
get(target, key){
10
// 收集依賴:綁定target的屬性與effect的關係
11
console.log(target, key)
12
return Reflect.get(target, key)
13
},
14
set(target, key, newValue){
15
// 觸發更新:通知之前收集的依賴,重新執行effect
7 collapsed lines
16
console.log(target, key, newValue)
17
return Reflect.set(target, key, newValue)
18
}
19
})
20
21
return proxy
22
}

我們來看一下,實際上的輸出值:

default

看來好像蠻接近的,但依照我們寫 ref 的經驗,我們還需要做鏈表相關邏輯。

先回顧一下我們的 ref 之前怎麼寫的:

1
export function trackRef(dep) {
2
if (activeSub) {
3
link(dep, activeSub)
4
}
5
}
6
7
export function triggerRef(dep) {
8
if (dep.subs) {
9
propagate(dep.subs)
10
}
11
}
  • get有一個trackRef函式,trackRef函式判斷是不是有effect(activeSub),有的話將依賴(dep)以及effect(activeSub)傳入link函式跟做鏈表關聯關係。
  • set有一個 triggerRef函式,triggerRef函式判斷是不是收集的依賴有effect,有的話就傳入propagate作觸發更新。

看來這個依賴(dep)很重要,那什麼是依賴?

1
class RefImpl {
2
_value;
3
[ReactiveFlags.IS_REF] = true
4
5
subs: Link
6
subsTail: Link
7
constructor(value) {
8
this._value = value
9
}
10
11
get value() {
12
if (activeSub) {
13
trackRef(this)
14
}
15
return this._value
7 collapsed lines
16
}
17
18
set value(newValue) {
19
this._value = newValue
20
triggerRef(this)
21
}
22
}

我們可以看到傳入只有

  • sub
  • subsTail

那我們可以認定只要有這兩個屬性,他就是一個 dep,那我們可以建立一個 Dep 類別,其他照 ref 的 trackRef 和 triggerRef 邏輯複製過來,並修改。

1
import { activeSub } from './effect'
2
import { link, propagate, Link } from './system'
3
4
function createReactiveObject(target){
5
// reactive 只處理物件
6
if(!isObject(target)) return target
7
8
// 建立 target 的代理物件
9
const proxy = new Proxy(target, {
10
get(target, key){
11
// 收集依賴:綁定target的屬性與effect的關係
12
track(target, key)
13
return Reflect.get(target, key)
14
},
15
set(target, key, newValue){
25 collapsed lines
16
// 觸發更新:通知之前收集的依賴,重新執行effect
17
trigger(target, key)
18
return Reflect.set(target, key, newValue)
19
}
20
})
21
22
return proxy
23
}
24
25
class Dep{
26
subs: Link
27
subsTail: Link
28
constructor
29
}
30
31
function track(target, key){
32
if(!activeSub)return
33
link(dep, activeSub) // 有問題
34
}
35
36
function trigger(target, key){
37
if (dep.subs) {
38
propagate(dep.subs) // 有問題
39
}
40
}

這邊有個地方要注意,觸發通知的話要先更新數值,再去通知重新執行,所以 set 這邊要這樣寫:

1
set(target, key, newValue){
2
const res = Reflect.set(target, key, newValue)
3
// 觸發更新:通知之前收集的依賴,重新執行effect
4
trigger(target, key)
5
return res
6
}

名稱重複,調整一下 system.ts interface 名稱。

1
interface Dependency {
2
subs: Link | undefined
3
subsTail: Link | undefined
4
}
5
6
export interface Link {
7
...
8
dep: Dependency
9
...
10
}

感覺新建一個Dep類別的實例,傳進 track就可以了,不過使用者傳入的 target 物件跟我們的新建的Dep似乎沒有關係。

看起來我們遇到了一些問題:

  • 我們不能再用一個 Dep 來管理所有依賴,必須為物件的每個屬性都維護一個 Dep。
  • 如何建立 target.a → Dep for a 的對應關係?
  • 如何在不污染原始 target 物件的情況下,儲存 target、key 與 Dep 之間的關聯?

為了解決這個問題,我們需要引入一個更複雜的資料結構來儲存,明天我們再接續探討。

Article title:Day 18 - Reactive:深入 Proxy 的設計思路
Article author:日安
Release time:27th September 2025