iT邦幫忙

2025 iThome 鐵人賽

DAY 3
2
Vue.js

從零到一打造 Vue3 響應式系統系列 第 3

Day 3 - 核心概念: 從「訂閱者模式」看響應式設計

  • 分享至 

  • xImage
  •  

banner

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

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

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

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  

  <script type="module">
    import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'

    const count = ref(0)

    effect(() => {
      console.log('count.value ==>', count.value);
    })

    setTimeout(() => {
      count.value++
    }, 1000)
  </script>
</body>
</html>

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

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

day3-01

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

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

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

class RefImpl {
  _value; // 保存實際數值
  constructor(value){
    this._value = value //儲存傳入 ref 的數值
  }
}

export function ref(value){
   return new RefImpl(value) // 建立一個 ref 實例
}
export function effect(fn){
  fn() // 執行傳入的函式
}
export * from './ref'
export * from './effect'

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

 // import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
    import { ref, effect } from '../dist/reactivity.esm.js'

    const count = ref(0)

    effect(() => {
      console.log('count.value ==>', count.value);
    })

    setTimeout(() => {
      count.value++
    }, 1000)

day3-02

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

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

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

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

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

響應式系統核心概念

const count = ref(0)

effect(() => {
  console.log('count.value ==>', count.value);
})

setTimeout(() => {
  count.value++
}, 1000)

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

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

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

// 路人甲訂閱(當他「閱讀」雜誌時,自動成為訂閱者)
effect(() => {
  console.log('count.value ==>', count.value); // 閱讀雜誌
})

// 出版社發行新版雜誌
setTimeout(() => {
  count.value++  // 新版發行,自動通知所有訂閱者
}, 1000)

Pub-Sub Pattern 發布訂閱模式

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

傳統發布訂閱模式

// 發布者(出版社)
class Publisher {
  constructor() {
    this.subscribers = []  // 訂閱者名單
  }
  
  // 訂閱方法
  subscribe(subscriber) {
    this.subscribers.push(subscriber)
    console.log(`${subscriber.name} 已訂閱`)
  }
  
  // 發布方法
  publish(content) {
    console.log(`發布新內容: ${content}`)
    this.subscribers.forEach(sub => {
      sub.notify(content)  // 通知所有訂閱者
    })
  }
}

// 訂閱者
class Subscriber {
  constructor(name) {
    this.name = name
  }
  
  notify(content) {
    console.log(`${this.name} 收到: ${content}`)
  }
}

// 使用範例
const magazine = new Publisher()
const 路人甲 = new Subscriber('路人甲')
const 路人乙 = new Subscriber('路人乙')

magazine.subscribe(路人甲)  // 路人甲訂閱
magazine.subscribe(路人乙)  // 路人乙訂閱

magazine.publish('AI 特刊')  // 發布新刊

圖解

day3-03

day3-04

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

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

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

2. 發布階段 (更新):

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

Vue 發布訂閱模式

day3-05

// 自動訂閱(依賴收集)
effect(() => {
  console.log(count.value) // 讀取即訂閱
})

// 修改時自動通知
count.value++ // 自動觸發更新

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

  • 自動訂閱(依賴收集階段)
    • 不需要手動呼叫 subscribe 方法
    • effect 讀取 ref.value 時,自動建立訂閱關係
    • ref 在被讀取時,自動收集當前的 effect 作為訂閱者
  • 自動發布(觸發更新階段)
    • 不需要手動呼叫 publish 方法
    • ref.value 被修改時,自動通知所有訂閱者
    • 相關的 effect 自動重新執行

同步更新《嘿,日安!》技術部落格


上一篇
Day 2 - 基礎建設: Monorepo 與 pnpm Workspace 環境搭建
下一篇
Day 4 - 核心概念:收集依賴、觸發更新
系列文
從零到一打造 Vue3 響應式系統10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言