1import { ref, effect } from '../dist/reactivity.esm.js'2
3const count = ref(0)4
5effect(() => {6 console.log('effect1', count.value)7})8
9effect(() => {10 console.log('effect2', count.value)11})12
13setTimeout(() => {14 count.value = 115}, 1000)
昨天,我們了解鏈表的核心觀念,現在要把這些概念結合起來。
首先讓我們從一個常見的場景開始:當一個響應式數據(ref
)同時被多個 effect
依賴時,會發生什麼?
我們預期他可以輸出如下:
1console.log('effect1', 0)2console.log('effect2', 0)3//1秒後4console.log('effect1', 1)5console.log('effect2', 1)
但實際上我們得到的是:
1console.log('effect1', 0)2console.log('effect2', 0)3//1秒後4console.log('effect2', 1)
發生什麼事?
結果很明顯:我們上次做的ref
實作,只能讓this.subs
屬性一次記住一個訂閱者,導致後來的effect
覆蓋前面。這會造成以下問題:
- 每次有新的
effect
訂閱時,會覆蓋掉前一個 - 導致只有最後一個
effect
能收到更新通知
1get value(){2 if(activeSub){3 this.subs = activeSub4 }5 return this._value6}
第一個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
。
一秒後更新觸發
1set value(newValue){2 this._value = newValue3 this.subs?.()4 }
- 執行
count.value = 1
- 進入 setter:
this._value = 1
。 - 呼叫
this.subs?.()
→ 直接呼叫目前存在於subs
的函式fn2
。 - 因為只有
fn2
被呼叫,所以只印出console.log('effect2', 1)
。
問題解決方案
接下來我們運用上次說的鏈表,來處理被覆蓋的問題,這邊我們使用雙向鏈表:
1// 定義鏈表節點結構2interface Link {3 // 保存 effect4 sub:Function5 // 下一個節點6 nextSub:Link7 // 上一個節點8 prevSub:Link9}10
11class RefImpl {12 _value;13 [ReactiveFlags.IS_REF] = true54 collapsed lines
14
15 subs:Link //訂閱者鏈表頭節點16 subsTail:Link //訂閱者鏈表尾節點17
18 constructor(value){19 this._value = value20 }21
22 get value(){23 if(activeSub){24 // 建立節點25 const newLink = {26 sub: activeSub,27 nextSub:undefined,28 prevSub:undefined29 }30
31 /**32 * 關聯鏈表關係33 * 1.如果有尾節點,表示鏈表現在有無數個節點,在鏈表尾部新增。34 * 2.如果沒有尾節點,表示是第一次關聯鏈表,第一個節點頭尾相同。35 */36 //37 if(this.subsTail){38 this.subsTail.nextSub = newLink39 newLink.prevSub = this.subsTail40 this.subsTail = newLink41 }else {42 this.subs = newLink43 this.subsTail = newLink44 }45 }46 return this._value47 }48
49 set value(newValue ){50 this._value = newValue51
52 // 取得頭節點53 let link = this.subs54 let queuedEffect = []55
56 // 遍歷整個鏈表的每一個節點57 // 把每個節點裡的 effect 函數放進陣列58 // 不是放節點本身,是放節點裡的 sub 屬性(effect 函數)59 while (link){60 queuedEffect.push(link.sub)61 link = link.nextSub62 }63
64 //觸發更新65 queuedEffect.forEach(effect => effect())66 }67}
解決後執行流程
初始化
- 初始化,在走到
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
- 觸發 setter
this._value = 1
。 - 從 頭節點 開始遍歷鏈表,把每個節點的
sub
(也就是 effect 函式)放進queuedEffect
:- 先推
effect1
,再推effect2
- 先推
queuedEffect.forEach(fn => fn())
依序執行:- 先跑
effect1()
→ 列印effect1 1
- 再跑
effect2()
→ 列印effect2 1
- 先跑
透過雙向鏈表,我們成功解決了訂閱者被覆蓋的問題。現在無論有多少個 effect
依賴,都能在資料變更時收到通知並更新。