在正式開始實作我們自己的響應式 API 之前,我們先建立一個簡單的測試環境,來觀察 Vue 官方 ref
和effect
的實際情況。
先在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 官方提供的版本,因此這個行為是完全正常的。
我們現在開始實作,現在知道有兩件事:
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)
執行後會發現,第一次的輸出是 undefined
,且一秒後沒有任何變化。這完全正常,畢竟我們還沒實作任何依賴追蹤的機制。
這次沒有成功,讓我們了解了確切的問題所在:
count.value
讀取到的是undefined
。這是因為我們還沒有定義當讀取 .value
時應該做什麼事 (缺少 getter 攔截)。count.value++
後,effect
內的函式沒有重新執行。這是因為effect
和count
之間沒有建立任何關聯(缺少訂閱機制)。為了解決這兩個問題,我們需要引入響應式系統中最核心的設計模式。
我們接下來要解決的核心問題:依賴收集和觸發更新。
const count = ref(0)
effect(() => {
console.log('count.value ==>', count.value);
})
setTimeout(() => {
count.value++
}, 1000)
參考上方程式碼,我們現在想要做的是進入頁面的時候,count
會輸出 0
,但我們一但修改了count
,effect
的函式輸出就會跟著改變,這也是我們在 Vue3 裡面很常做的事,所以我們可以知道響應式的核心概念就是:當資料發生改變,相關的副作用會自動更新。
這個「資料改變,相關操作自動執行」的模式,其實可以用一個生活化的例子來比喻:出版社與訂閱者。
count.value
)就自動成為訂閱者 ← 這是依賴收集
count.value
被修改)// 出版社(儲存資料 + 管理訂閱者)
const count = ref(0)
// 路人甲訂閱(當他「閱讀」雜誌時,自動成為訂閱者)
effect(() => {
console.log('count.value ==>', count.value); // 閱讀雜誌
})
// 出版社發行新版雜誌
setTimeout(() => {
count.value++ // 新版發行,自動通知所有訂閱者
}, 1000)
這個「出版社-訂閱者」的互動模式,在軟體設計中被稱為發布-訂閱模式 (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 特刊') // 發布新刊
這個模式的運作流程可以分為兩個主要階段:
1. 訂閱階段 (初始化):
2. 發布階段 (更新):
// 自動訂閱(依賴收集)
effect(() => {
console.log(count.value) // 讀取即訂閱
})
// 修改時自動通知
count.value++ // 自動觸發更新
Vue 發布訂閱模式,與一般傳統發布訂閱模式不同
ref.value
時,自動建立訂閱關係effect
作為訂閱者ref.value
被修改時,自動通知所有訂閱者effect
自動重新執行同步更新《嘿,日安!》技術部落格