嘿,日安!

Day 9 - Effect:調度器實作應用

18th September 2025
Front-end
Vue3
IT鐵人賽2025
從零到一打造 Vue3 響應式系統
Last updated:18th September 2025
8 Minutes
1458 Words

到目前為止,我們的 effect 會在依賴的資料發生變化時,會立刻重新執行。 這種簡單直接的模式在很多情況下都有效,但當遇到密集且連續性的資料變更時,它可能會引發不必要的效能問題。

為什麼需要 Effect 調度器?

1
const count = ref(0)
2
effect(() => {
3
console.log('渲染元件:', count.value)
4
//複雜的 DOM 操作
5
})// 連續修改
6
count.value = 1 // 觸發渲染
7
count.value = 2 // 又觸發渲染
8
count.value = 3 // 再次觸發渲染

在上方案例我們可以看到,如果有複雜的 DOM 操作,造成連續觸發重新渲染三次,但其實我們只要最後一次,這時候我們就需要調度器處理。

什麼是 Effect 調度器?

調度器是一個控制 effect 執行時機的機制:

  • 沒有調度器:資料變化 → 立即執行 effect
  • 有調度器:資料變化 → 調度器決定何時/如何執行

特性

避免同步連續觸發多次更新

1
// 避免同步連續觸發多次更新
2
const scheduler = (job) => {
3
Promise.resolve().then(job) // 下一個微任務執行
4
}
5
6
effect(() => {
7
console.log(count.value)
8
}, { scheduler })
9
10
count.value = 1 // 不會立即執行
11
count.value = 2 // 不會立即執行
12
count.value = 3 // 只有最後一次會在微任務中執行

Vue 元件更新調度

1
effect(() => {
2
// 元件渲染邏輯
3
}, {
4
scheduler: queueJob // 加入更新隊列,而不是立即更新
5
})

防抖、節流

1
const debounceScheduler = debounce((job) => job(), 100)
2
3
effect(() => {
4
// 高頻觸發的邏輯
5
}, { scheduler: debounceScheduler })

調度器用法

1
import { ref, effect } from '../dist/reactivity.esm.js'
2
3
const count = ref(0)
4
5
effect(()=>{
6
console.log('Effect', count.value)
7
},{
8
scheduler(){
9
console.log('觸發調度器')
10
}
11
})
12
13
setTimeout(()=>{
14
count.value = 1
15
}, 1000)

目前效果

1
Effect 0
2
Effect 1

預期效果

使用調度器:

  • setTimeout 賦值不再輸出
1
Effect 0
2
// 觸發調度器,因此一秒後不輸出 'Effect', 1

Class 類別知識補充

要實現這個選用的調度器,我們需要利用 JavaScript Class 的特性。

我們先來補充這個知識:

  • 一般的類別
1
class Person{
2
constructor(name){
3
this.name = name
4
}
5
6
sayHi(){
7
console.log('我是原型方法', this.name)
8
}
9
}
10
11
const p = new Person('張三')
12
13
p.sayHi()
14
// 輸出:我是原型方法 張三

建立實例屬性被覆蓋

  • 先建立實例 p
  • sayHi 方法重新賦予,於是被覆蓋
  • 因為實例屬性的方法優於原型屬性的方法
  • 如果沒有實例方法,才會往原型鏈去找原型方法
1
class Person{
2
constructor(name){
3
this.name = name
4
}
5
6
sayHi(){
7
console.log('我是原型方法', this.name)
8
}
9
}
10
11
const p = new Person('張三')
12
13
p.sayHi = function(){
14
console.log('我是實例屬性', this.name)
15
}
3 collapsed lines
16
17
p.sayHi()
18
// 輸出:我是實例屬性 張三

實作調度器

要求

  1. 更新時,觸發 scheduler
  2. 沒有傳入 scheduler,仍然要執行 run()

實作思路

  1. 使用者傳入 scheduler 方法,此為可選方法
  2. 因此 scheduler 有可能會被覆蓋(可參考剛剛提到的 Class 類別知識補充)
  3. 為了保證觸發更新可以正常實作,新建一個 notify 方法
1
export let activeSub;
2
3
export class ReactiveEffect {
4
constructor(public fn){
5
6
}
7
8
run(){
9
const prevSub = activeSub
10
activeSub = this
11
12
try{
13
return this.fn()
14
15
}finally{
44 collapsed lines
16
activeSub = prevSub
17
}
18
}
19
20
/*
21
* 如果依賴資料發生變化,通知更新。
22
*/
23
notify(){
24
this.scheduler()
25
}
26
27
/*
28
* 預設調用 run 方法,
29
* 如果用戶傳入覆蓋調用器,那以用戶的為主
30
* 因為實例屬性優於原型屬性
31
*/
32
33
scheduler(){
34
this.run()
35
}
36
37
}
38
39
export function effect(fn, options){
40
41
const e = new ReactiveEffect(fn)
42
// scheduler
43
Object.assign(e, options)
44
e.run()
45
46
/*
47
* 綁定 this
48
* 也可替換為 const runner = () => e.run()
49
* 但不能使用 const runner = e.run()
50
* 會遺失 this
51
*/
52
const runner = e.run.bind(e)
53
54
//將 effect 實例,放入函式屬性
55
runner.effect = e
56
57
return runner
58
59
}

propagate函式中更改為執行 notify 方法,確保可以執行

1
export function propagate (subs){
2
....
3
// 更改為執行 notify 方法
4
// 因為 scheduler 方法有可能會被覆蓋
5
// 因此使用 notify 確保可以執行
6
queuedEffect.forEach(effect => effect.notify())
7
}

為什麼回傳 runner 時需要 e.run.bind(e)這麼處理? 如果直接回傳e.run會發生什麼?這就涉及到了 this 指向問題。

遺失 this 是指什麼

請參考下方範例:

1
export function effect(fn, options){
2
const e = new ReactiveEffect(fn)
3
Object.assign(e, options)
4
e.run()
5
6
return e.run //遺失 this
7
}
8
9
// 使用時
10
const runner = effect(() => console.log('effect'))
11
runner() // Error! this 是 undefined 或 window

圖解 notify() 執行步驟

default

執行原型方法(左)

effect 的預設行為,當我們像這樣使用它時: effect(() => { ... })

  1. 資料變化 → propagate: 當響應式資料的值被修改時,會觸發其 setter,最終由 propagate 函式開始遍歷依賴資料的 effect。

  2. propagateeffect.notify()propagate 的迴圈裡面,我們統一呼叫 effect.notify(),讓它作為更新的固定入口點。

  3. effect.notify()scheduler() notify() 內部會去呼叫 this.scheduler()。在這個情況下,因為我們建立 effect 時沒有提供任何 options,所以 effect 實例上並不存在自己的 scheduler 屬性。

  4. scheduler()run() 根據 JavaScript 的原型鏈規則,它會回去尋找 ReactiveEffect 原型上的 scheduler() 方法。 我們預設的 scheduler() 方法,就是直接呼叫 this.run()。因此,effect 的核心邏輯被立即執行。

使用調度器(右)

  1. 資料變化 → propagateeffect.notify() 前三個步驟跟原形狀況完全相同:資料變更,propagate 遍歷並呼叫 effect.notify()

  2. effect.notify()scheduler() notify() 內部會呼叫 this.scheduler()。但關鍵在於,這次我們在建立 effect 時,透過 options 傳入了一個自訂的 scheduler 函式。

因此 Object.assign(e, options) 這個會把函式作為一個實例屬性附加到 effect 物件上。

  1. scheduler() → 使用者的調度器邏輯 根據 JavaScript 的優先級,實例屬性大於原型方法。因此,JavaScript 在 effect 實例上直接找到這個自訂的 scheduler 並執行。

今天,我們透過引入調度器,將 effect 的核心邏輯以及執行策略進行了分離,當中包含 fn 做了什麼,以及 scheduler 決定什麼時候做。

Article title:Day 9 - Effect:調度器實作應用
Article author:日安
Release time:18th September 2025