const count = ref(0)
effect(() => {
console.log('count.value ==>', count.value);
})
setTimeout(() => {
count.value++
}, 1000)
昨天我們的目標是讓一段簡單的 ref
和 effect
程式碼能夠自動響應。
count.value ==> 0
count.value ==> 1
然而,我們初次實作遇到問題:無法正確取值(undefined
),也無法在值變更後觸發更新。
為了解決這個問題,我們要去思考 ref
需要做的事:
ref
要怎麼知道誰在讀取?ref
要怎麼知道要通知誰?// 原本的程式碼
class RefImpl {
_value;
constructor(value){
this._value = value
}
}
現在要加入 getter 和 setter,讓 count.value
能正常運作:
class RefImpl {
_value;
constructor(value){
this._value = value
}
// 新增 getter:讀取 value 時觸發
get value(){
console.log('有人讀取了 value!')
return this._value
}
// 新增 setter:設定 value 時觸發
set value(newValue){
console.log('有人修改了 value!')
this._value = newValue
}
}
現在看起來 count.value
可以正常返回值,但這個時候還是不知道讀取誰、通知誰。
export function effect(fn){
fn()
}
這時候我們需要儲存當前執行的 effect 函式。
//effect.ts
// 用來保存目前現在正在執行的 effect 函式
export let activeSub;
export function effect(fn){
activeSub = fn
activeSub()
activeSub = undefined
}
這個新版的 effect
函式做了三件事:
fn
之前,先將它賦值給全域變數activeSub
。fn()
。如果在執行過程中讀取了某個 ref
的.value
,這個 ref
就能透過activeSub
知道是誰在讀取它。activeSub
清空 (設為undefined
)。這非常重要,它能確保只有在 effect
的執行期間,讀取ref
的行為才會被視為依賴收集。現在我們要讓 ref
能夠:
我們可以在 getter 在讀取值的時候,判斷activeSub
是否存在,來確認當下情況是不是要收集依賴。
// ref.ts
import { activeSub } from './effect'
class RefImpl {
_value;
subs; // 新增:用來儲存訂閱者
constructor(value){
this._value = value
}
// 新增 getter:讀取 value 時觸發
get value(){
// 依賴收集:如果有 activeSub,就記錄下來
if(activeSub){
this.subs = activeSub
}
return this._value
}
// 新增 setter:設定 value 時觸發
set value(newValue){
// 觸發更新:如果有訂閱者,就執行它
if(this.subs){
this.subs() // 重新執行 effect
} // 可簡寫 this.subs?.()
}
}
為了方便在後續的系統中判斷一個變數是否為ref
物件,我們可以新增一個輔助函式 isRef
和一個內部標記:
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
class RefImpl {
_value;
subs; // 新增:用來儲存訂閱者
[ReactiveFlags.IS_REF] = true
...
}
export function isRef(value){
return !!(value && value[ReactiveFlags.IS_REF])
}
現在,讓我們將所有部分串連起來,完整地模擬執行流程。
剛開始進入頁面。
import { ref, effect } from '../dist/reactivity.esm.js'
const count = ref(0)
程式執行:const count = ref(0)
ref(0)
,建立一個 RefImpl
實例。count
實例的內部狀態為:
_value: 0
subs: undefined
__v_isRef: true
呼叫 effect
函式,並傳入匿名函式 fn
作為參數。
effect(() => {
console.log('effect', count.value)
})
effect
函式內部
export let activeSub;
export function effect(fn){
activeSub = fn
activeSub()
activeSub = undefined
}
設定 activeSub:
activeSub
被賦值為 fn
:activeSub = fn
。
立刻執行 fn()
console.log('effect', count.value)
count
實例的 get value()
。getter
內部:if(activeSub)
條件成立,activeSub
正是我們的 fn
。
if(activeSub){
this.subs = activeSub
}
this.subs = activeSub
。count
實例透過 subs
屬性,記住了是 fn
在依賴它。getter
回傳 this._value
(也就是 0)。console.log
輸出:effect 0
。activeSub = undefined
(執行完成後清空,沒有 effect 在執行)。
此時
count.subs
就是傳入 effect
的函式。count
→ effect(fn)
。set value(newValue)
被呼叫,this._value = 1
。this.subs?.()
若有訂閱者就呼叫(這裡就是前面存起來的 effect
函式)effect
函式再次執行
console.log('effect', count.value)
→ 讀 getter → 看見沒有 activeSub
,所以不會收集依賴。effect(fn)
的包裝流程,所以第二次之後執行 effect 時 activeSub
是 undefined
。console.log
輸出:effect 1
。這樣我們就完成響應式依賴收集的最小可行版本。
完整程式碼
ref.ts
import { activeSub } from './effect'
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
class RefImpl {
_value; // 保存實際數值
// ref 標記,證實是個 ref
[ReactiveFlags.IS_REF] = true
subs
constructor(value){
this._value = value
}
// 收集依賴
get value(){
// 當有人訪問的時候,可以取得 activeSub
if(activeSub){
//當有 activeSub 儲存值,以便更新後觸發
this.subs = activeSub
}
return this._value
}
// 觸發更新
set value(newValue){
this._value = newValue
// 通知 effect 重新執行,取得最新的 value
this.subs?.()
}
}
export function ref(value){
return new RefImpl(value)
}
export function isRef(value){
return !!(value && value[ReactiveFlags.IS_REF])
}
effect.ts
// 用來保存目前現在正在執行的 effect 函式
export let activeSub;
export function effect(fn){
activeSub = fn
activeSub()
activeSub = undefined
}
同步更新《嘿,日安!》技術部落格