import { ref, effect } from '../dist/reactivity.esm.js'
const count = ref(0)
effect(() => {
console.log('effect1', count.value)
})
effect(() => {
console.log('effect2', count.value)
})
setTimeout(() => {
count.value = 1
}, 1000)
昨天,我們了解鏈表的核心觀念,現在要把這些概念結合起來。
首先讓我們從一個常見的場景開始:當一個響應式數據(ref
)同時被多個 effect
依賴時,會發生什麼?
我們預期他可以輸出如下:
console.log('effect1', 0)
console.log('effect2', 0)
//1秒後
console.log('effect1', 1)
console.log('effect2', 1)
但實際上我們得到的是:
console.log('effect1', 0)
console.log('effect2', 0)
//1秒後
console.log('effect2', 1)
結果很明顯:我們上次做的ref
實作,只能讓this.subs
屬性一次記住一個訂閱者,導致後來的effect
覆蓋前面。這會造成以下問題:
effect
訂閱時,會覆蓋掉前一個effect
能收到更新通知get value(){
if(activeSub){
this.subs = activeSub
}
return this._value
}
effect
加入console.log('effect1', 0)
effect(fn1)
,activeSub = fn1
,然後立刻執行 fn1()
。fn1
讀取 count.value
→ 進入 getter:
activeSub
存在 → this.subs = activeSub
(把 subs
指到 fn1
)。0
,所以印出 effect1 0
。effect(fn1)
結束,把 activeSub
清回 undefined
。effect
加入console.log('effect2', 0)
effect(fn2)
,activeSub = fn2
,執行 fn2()
。fn2
讀 count.value
→ getter:
activeSub
存在 → this.subs = activeSub
覆蓋掉 fn1
,現在 subs === fn2
。0
,印出 effect2 0
。effect(fn2)
結束,把 activeSub
清回 undefined
。set value(newValue){
this._value = newValue
this.subs?.()
}
count.value = 1
this._value = 1
。this.subs?.()
→ 直接呼叫目前存在於 subs
的函式 fn2
。fn2
被呼叫,所以只印出 console.log('effect2', 1)
。接下來我們運用上次說的鏈表,來處理被覆蓋的問題,這邊我們使用雙向鏈表:
//ref.ts
// 定義鏈表節點結構
interface Link {
// 保存 effect
sub:Function
// 下一個節點
nextSub:Link
// 上一個節點
prevSub:Link
}
class RefImpl {
_value;
[ReactiveFlags.IS_REF] = true
subs:Link //訂閱者鏈表頭節點
subsTail:Link //訂閱者鏈表尾節點
constructor(value){
this._value = value
}
get value(){
if(activeSub){
// 建立節點
const newLink = {
sub: activeSub,
nextSub:undefined,
prevSub:undefined
}
/**
* 關聯鏈表關係
* 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。
* 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。
*/
//
if(this.subsTail){
this.subsTail.nextSub = newLink
newLink.prevSub = this.subsTail
this.subsTail = newLink
}else {
this.subs = newLink
this.subsTail = newLink
}
}
return this._value
}
set value(newValue ){
this._value = newValue
// 取得頭節點
let link = this.subs
let queuedEffect = []
// 遍歷整個鏈表的每一個節點
// 把每個節點裡的 effect 函數放進陣列
// 不是放節點本身,是放節點裡的 sub 屬性(effect 函數)
while (link){
queuedEffect.push(link.sub)
link = link.nextSub
}
//觸發更新
queuedEffect.forEach(effect => effect())
}
}
effect
之前,頭尾節點都是 undefined
。effect
加入effect(fn1)
訪問 count
activeSub = effect1
,馬上執行 effect1()
。effect1
讀取 count.value
→ 進 get
:
activeSub
存在 → 建立 newLink(effect1)
。subsTail
為undefined
,所以把 頭節點跟尾節點都指向 newLink(effect1)
。effect1 0
。activeSub
:activeSub = undefined
。effect
加入effect(fn2)
訪問 count
activeSub = effect2
,執行 effect2()
。effect2
讀取 count.value
→ 觸發 getter
:
activeSub
存在 → 建立 newLink(effect2)
。subsTail
存在(指向 effect1
),所以把 effect2
掛在尾端:
effect1.next = effect2
effect2.prev = effect1
subsTail = effect2
effect2 0
。activeSub
:activeSub = undefined
。count.value = 1
this._value = 1
。sub
(也就是 effect 函式)放進 queuedEffect
:
effect1
,再推 effect2
queuedEffect.forEach(fn => fn())
依序執行:
effect1()
→ 列印 effect1 1
effect2()
→ 列印 effect2 1
透過雙向鏈表,我們成功解決了訂閱者被覆蓋的問題。現在無論有多少個 effect
依賴,都能在資料變更時收到通知並更新。
同步更新《嘿,日安!》技術部落格