嘿,日安!

Day 19 - Reactive:reactive 的基礎實作

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

上一次我們提到:

  • 每個物件的每個屬性都需要自己的 Dep。
  • 如何建立 target.a → Dep 的對應關係?
  • 如何在不污染原始物件的情況下儲存這個關係?

我們可以先來做一個簡單的比較

Ref、Reactive 比較

RefReactive
資料結構單一值物件(多個屬性)
依賴儲存直接在實例上(this)需要一個外部的的儲存機制
一個 ref一個 Dep多個 Dep(每個屬性一個)

Ref 可以用 this 因為它就是一個實例。

但 Reactive 的每個屬性都需要自己的 Dep,要存在哪?

那我們這時候可以建立一個 Weak Map 物件。

什麼是 Weak Map?

  • WeakMap 是一種 鍵值對集合(key-value pairs)。
  • key 只能是物件(不能是字串、數字、布林),value 可以是任意型別。
  • 弱引用(weak reference):如果一個物件只被 WeakMap 當 key 使用,而程式中沒有其它變數參考它,這個物件就會被垃圾回收(GC)自動清掉。

看來來正好適合我們去做關聯關係。

核心概念

WeakMap 建立一個全域的 targetMap,它的三層巢狀結構如下:

  1. 第一層 targetMap (WeakMap)key 是原始的目標物件 targetvalue 是第二層的 depsMap{ target => depsMap }
  2. 第二層 depsMap (Map)keytarget 物件中的屬性名 keyvalue 是第三層的 dep{ key => dep }
  3. 第三層 dep (Dep 實例) :依賴的容器,儲存了所有訂閱該屬性變更的 effect{ subs, subsTail }

為何不直接用 Map?

因為如果使用一般的 Map,Map 會一直保持對 target 物件的引用,只要它還存在於 Map 中,GC 就無法回收,導致記憶體洩漏。

1
const targetMap = new WeakMap()

它的結構會長這樣

1
target = {
2
a:0,
3
b:1
4
}
5
6
tagetMap = {
7
[obj]:{
8
a:Dep,
9
b:Dep
10
}
11
}

這樣子 Dep 跟 target 就有關係了,一個屬性對應一個 Dep,我們可以通過 target 找到 obj 對應的物件,還可以透過屬性a找到Dep實例,這樣就可以建立關聯關係。

targetMap 的 key 是 obj ,value 是一個 map,這個 map 裡面,map 裡面的 key 就是 obj 的屬性,value 就是對應的 dep,這樣就可以收集依賴。

default

等到需要觸發更新,透過 obj 找到對應的 Map,再透過 key 找到對應的 Dep 通知更新。

收集依賴

收集依賴有分為首次收集依賴,跟之前已經收集過了,所以我們可以這樣寫。

1
function track(target, key){
2
if(!activeSub)return
3
// 透過 targetMap 取得 target 的依賴
4
let depsMap = targetMap.get(target)
5
6
//首次收集依賴,之前沒有收集過,就新建一個
7
// key:obj / value:depsMap
8
if(!depsMap){
9
depsMap = new Map()
10
targetMap.set(target, depsMap)
11
}
12
13
let dep = depsMap.get(key)
14
15
// 收集依賴:第一次建立物件依賴關聯,並且保存到depsMap中
10 collapsed lines
16
// key:key / value:Dep
17
if(!dep){
18
dep = new Dep()
19
depsMap.set(key, dep)
20
}
21
22
console.log(targetMap, dep)
23
24
link(dep, activeSub)
25
}

可以 console.log 看起來targetMapdep

default

看起來的確是我們想的那樣,接下來做觸發更新。

觸發更新

觸發更新的話,原本是寫去找依賴 (dep)的 effect(sub),如果找到,就傳入propagate,現在我們的 dep 都存入了depsMap,那我們就理應去depsMap找:

1
function trigger(target, key){
2
const depsMap = targetMap.get(target)
3
// 如果 depsMap 不存在,表示沒有收集過依賴,直接返回
4
if(!depsMap)return
5
6
const dep = depsMap.get(key)
7
// 如果依賴不存在,表示這個 key 沒有在effect中被使用過,直接返回
8
if(!dep)return
9
10
// 找到依賴,觸發更新
11
propagate(dep.subs)
12
}

接下來回去看我們的範例,初始化成功輸出0,一秒之後輸出1。

default

看起來成功了,接下來我們來測試 reactivegetter 的響應追蹤:

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
get count(){
7
return this.a
8
}
9
})
10
effect(() => {
11
console.log(state.count)
12
})
13
14
setTimeout(() => {
15
state.a = 1
1 collapsed line
16
}, 1000)

預期結果是先輸出 0,一秒後輸出 1。但實際執行後,我們發現只有初始的 0 被輸出,state.a = 1 的更新並未觸發 effect

透過在 track 函式中輸出 (target, key),發現只有 count 屬性被追蹤了,a 屬性並沒有。

原因在於 return this.a 上,在 getter 內部,this 預設指向的是原始的 target 物件,而不是我們的 proxy 物件。

因此 this.a 的取值過程繞過了 Proxygeta 屬性的依賴自然也沒辦法收集。

1
const state = reactive({
2
a: 0,
3
get count() {
4
return this.a // this 應該指向誰?
5
}
6
})

它應該要指向我們的 Proxy 物件而不是原始物件,這樣它在觸發getter的時候,才會執行track(target, key)

那我們要怎麼做?

Proxyhandler 提供第三個參數 receiver,它指向的就是 proxy 物件本身,因此我們只需要將它傳遞給 Reflect.get 就可以修正 this 的指向。

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

這樣我們就完成,也可以看到初始化 console.log 輸出0,一秒之後輸出1。

執行步驟

default

回顧我們今天:

  • 我們引入了以 WeakMap 為核心的 targetMap 資料結構,解決在不污染原始物件的前提下,為多屬性物件管理各自依賴。
  • 我們實作與 targetMap 配套的 tracktrigger 函式。
  • 我們利用 Proxyreceiver 參數,修正 getterthis 指向的問題。
Article title:Day 19 - Reactive:reactive 的基礎實作
Article author:日安
Release time:28th September 2025