嘿,日安!

Day 3 - 訂閱者模式:響應式設計基礎

12th September 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:14th September 2025
8 Minutes
1478 Words

在正式開始實作我們自己的響應式 API 之前,我們先建立一個簡單的測試環境,來觀察 Vue 官方 refeffect的實際情況。 先在packages/reactivity/目錄下新增一個example資料夾,並建立index.html檔案:

  • 我們預期進入頁面時,控制台會輸出 0
  • 一秒後,控制台會輸出 1

接著本地啟動這個 html 檔案,這邊可以使用 live server 套件,即可在本地中運行。

1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="UTF-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6
<title>Document</title>
7
</head>
8
<body>
9
10
11
<script type="module">
12
import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
13
14
const count = ref(0)
15
10 collapsed lines
16
effect(() => {
17
console.log('count.value ==>', count.value);
18
})
19
20
setTimeout(() => {
21
count.value++
22
}, 1000)
23
</script>
24
</body>
25
</html>

我們可以看到 console 控制台中進入頁面時,出現輸出 0,並且一秒後再輸出 1

由於我們目前使用的是 Vue 官方提供的版本,因此這個行為是完全正常的。

default

我們現在開始實作,現在知道有兩件事:

  • 我們進入頁面時,傳入 effect 的函式會執行
  • ref 函式會接收一個初始值,並回傳一個物件。我們可以透過該物件的 .value 屬性來存取或修改這個值。

所以我們先在 package/reactivity/src下新增兩個檔案,分別是 ref.ts 以及effect.ts,並且在 index.ts 集中匯出。

1
class RefImpl {
2
_value; // 保存實際數值
3
constructor(value){
4
this._value = value //儲存傳入 ref 的數值
5
}
6
}
7
8
export function ref(value){
9
return new RefImpl(value) // 建立一個 ref 實例
10
}
1
export function effect(fn){
2
fn() // 執行傳入的函式
3
}
1
export * from './ref'
2
export * from './effect'

接著我們把官方的引用註解,引入我們的 dist 檔案,看看是否成功。

1
// import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
2
import { ref, effect } from '../dist/reactivity.esm.js'
3
4
const count = ref(0)
5
6
effect(() => {
7
console.log('count.value ==>', count.value);
8
})
9
10
setTimeout(() => {
11
count.value++
12
}, 1000)

default

執行後會發現,第一次的輸出是 undefined,且一秒後沒有任何變化。這完全正常,畢竟我們還沒實作任何依賴追蹤的機制。

這次沒有成功,讓我們了解了確切的問題所在:

  1. 無法取值: count.value讀取到的是undefined。這是因為我們還沒有定義當讀取 .value 時應該做什麼事 (缺少 getter 攔截)。
  2. 沒有更新: 修改 count.value++ 後,effect內的函式沒有重新執行。這是因為effectcount 之間沒有建立任何關聯(缺少訂閱機制)。

為了解決這兩個問題,我們需要引入響應式系統中最核心的設計模式。

我們接下來要解決的核心問題:依賴收集觸發更新

響應式系統核心概念

1
const count = ref(0)
2
3
effect(() => {
4
console.log('count.value ==>', count.value);
5
})
6
7
setTimeout(() => {
8
count.value++
9
}, 1000)

參考上方程式碼,我們現在想要做的是進入頁面的時候,count 會輸出 0,但我們一但修改了counteffect 的函式輸出就會跟著改變,這也是我們在 Vue3 裡面很常做的事,所以我們可以知道響應式的核心概念就是:當資料發生改變,相關的副作用會自動更新。

這個「資料改變,相關操作自動執行」的模式,其實可以用一個生活化的例子來比喻:出版社與訂閱者

  1. 路人甲(effect 函式) 訂閱了科技雜誌
    • 希望出版社將雜誌自動送到他家,不用他去催促
    • 只要看雜誌(讀取 count.value)就自動成為訂閱者 ← 這是依賴收集
  2. 出版社(ref) 管理雜誌內容
    • 擁有所有訂閱者的名單 ← 依賴收集的結果
    • 負責儲存最新的雜誌內容(資料值)
  3. 自動配送機制
    • 當雜誌有新版(count.value 被修改)
    • 出版社會自動寄送雜誌給所有訂閱者(執行 effect) ← 這是觸發更新
1
// 出版社(儲存資料 + 管理訂閱者)
2
const count = ref(0)
3
4
// 路人甲訂閱(當他「閱讀」雜誌時,自動成為訂閱者)
5
effect(() => {
6
console.log('count.value ==>', count.value); // 閱讀雜誌
7
})
8
9
// 出版社發行新版雜誌
10
setTimeout(() => {
11
count.value++ // 新版發行,自動通知所有訂閱者
12
}, 1000)

Pub-Sub Pattern 發布訂閱模式

這個「出版社-訂閱者」的互動模式,在軟體設計中被稱為發布-訂閱模式 (Publish-Subscribe Pattern),或簡稱 Pub-Sub。

傳統發布訂閱模式

1
// 發布者(出版社)
2
class Publisher {
3
constructor() {
4
this.subscribers = [] // 訂閱者名單
5
}
6
7
// 訂閱方法
8
subscribe(subscriber) {
9
this.subscribers.push(subscriber)
10
console.log(`${subscriber.name} 已訂閱`)
11
}
12
13
// 發布方法
14
publish(content) {
15
console.log(`發布新內容: ${content}`)
26 collapsed lines
16
this.subscribers.forEach(sub => {
17
sub.notify(content) // 通知所有訂閱者
18
})
19
}
20
}
21
22
// 訂閱者
23
class Subscriber {
24
constructor(name) {
25
this.name = name
26
}
27
28
notify(content) {
29
console.log(`${this.name} 收到: ${content}`)
30
}
31
}
32
33
// 使用範例
34
const magazine = new Publisher()
35
const 路人甲 = new Subscriber('路人甲')
36
const 路人乙 = new Subscriber('路人乙')
37
38
magazine.subscribe(路人甲) // 路人甲訂閱
39
magazine.subscribe(路人乙) // 路人乙訂閱
40
41
magazine.publish('AI 特刊') // 發布新刊

圖解

default

default

這個模式的運作流程可以分為兩個主要階段:

1. 訂閱階段 (初始化):

  • 註冊: 訂閱者 (Subscriber) 需要主動向發布者 (Publisher) 進行註冊。
  • 收集: 發布者將所有訂閱者的資訊收集起來,存放在一個名單中。

2. 發布階段 (更新):

  • 發布: 當有新內容發布時,發布者會發出通知。
  • 通知: 發布者會遍歷訂閱者名單,將新內容逐一發送給所有訂閱者。

Vue 發布訂閱模式

default

1
// 自動訂閱(依賴收集)
2
effect(() => {
3
console.log(count.value) // 讀取即訂閱
4
})
5
6
// 修改時自動通知
7
count.value++ // 自動觸發更新

Vue 發布訂閱模式,與一般傳統發布訂閱模式不同

  • 自動訂閱(依賴收集階段)
    • 不需要手動呼叫 subscribe 方法
    • effect 讀取 ref.value 時,自動建立訂閱關係
    • ref 在被讀取時,自動收集當前的 effect 作為訂閱者
  • 自動發布(觸發更新階段)
    • 不需要手動呼叫 publish 方法
    • ref.value 被修改時,自動通知所有訂閱者
    • 相關的 effect 自動重新執行
Article title:Day 3 - 訂閱者模式:響應式設計基礎
Article author:日安
Release time:12th September 2025