到目前為止,我們的 effect 會在依賴的資料發生變化時,會立刻重新執行。
這種簡單直接的模式在很多情況下都有效,但當遇到密集且連續性的資料變更時,它可能會引發不必要的效能問題。
const count = ref(0)
effect(() => {
console.log('渲染元件:', count.value)
//複雜的 DOM 操作
})// 連續修改
count.value = 1 // 觸發渲染
count.value = 2 // 又觸發渲染
count.value = 3 // 再次觸發渲染
在上方案例我們可以看到,如果有複雜的 DOM 操作,造成連續觸發重新渲染三次,但其實我們只要最後一次,這時候我們就需要調度器處理。
調度器是一個控制 effect 執行時機的機制:
// 避免同步連續觸發多次更新
const scheduler = (job) => {
Promise.resolve().then(job) // 下一個微任務執行
}
effect(() => {
console.log(count.value)
}, { scheduler })
count.value = 1 // 不會立即執行
count.value = 2 // 不會立即執行
count.value = 3 // 只有最後一次會在微任務中執行
effect(() => {
// 元件渲染邏輯
}, {
scheduler: queueJob // 加入更新隊列,而不是立即更新
})
const debounceScheduler = debounce((job) => job(), 100)
effect(() => {
// 高頻觸發的邏輯
}, { scheduler: debounceScheduler })
import { ref, effect } from '../dist/reactivity.esm.js'
const count = ref(0)
effect(()=>{
console.log('Effect', count.value)
},{
scheduler(){
console.log('觸發調度器')
}
})
setTimeout(()=>{
count.value = 1
}, 1000)
Effect 0
Effect 1
使用調度器:
setTimeout
賦值不再輸出Effect 0
// 觸發調度器,因此一秒後不輸出 'Effect', 1
要實現這個選用的調度器,我們需要利用 JavaScript Class 的特性。
我們先來補充這個知識:
class Person{
constructor(name){
this.name = name
}
sayHi(){
console.log('我是原型方法', this.name)
}
}
const p = new Person('張三')
p.sayHi()
// 輸出:我是原型方法 張三
p
sayHi
方法重新賦予,於是被覆蓋class Person{
constructor(name){
this.name = name
}
sayHi(){
console.log('我是原型方法', this.name)
}
}
const p = new Person('張三')
p.sayHi = function(){
console.log('我是實例屬性', this.name)
}
p.sayHi()
// 輸出:我是實例屬性 張三
scheduler
scheduler
,仍然要執行 run()
notify
方法export let activeSub;
export class ReactiveEffect {
constructor(public fn){
}
run(){
const prevSub = activeSub
activeSub = this
try{
return this.fn()
}finally{
activeSub = prevSub
}
}
/*
* 如果依賴資料發生變化,通知更新。
*/
notify(){
this.scheduler()
}
/*
* 預設調用 run 方法,
* 如果用戶傳入覆蓋調用器,那以用戶的為主
* 因為實例屬性優於原型屬性
*/
scheduler(){
this.run()
}
}
export function effect(fn, options){
const e = new ReactiveEffect(fn)
// scheduler
Object.assign(e, options)
e.run()
/*
* 綁定 this
* 1. 可替換為 const runner = () => e.run()
* 2. 不能使用 const runner = e.run
* 因為當之後呼叫 runner() 時,會遺失 this
*/
const runner = e.run.bind(e)
//將 effect 實例,放入函式屬性
runner.effect = e
return runner
}
propagate
函式中更改為執行 notify
方法,確保可以執行
export function propagate (subs){
....
// 更改為執行 notify 方法
// 因為 scheduler 方法有可能會被覆蓋
// 因此使用 notify 確保可以執行
queuedEffect.forEach(effect => effect.notify())
}
為什麼回傳 runner 時需要 e.run.bind(e)
這麼處理?
如果直接回傳e.run
會發生什麼?這就涉及到了 this 指向問題。
請參考下方範例:
export function effect(fn, options){
const e = new ReactiveEffect(fn)
Object.assign(e, options)
e.run()
return e.run //遺失 this
}
// 使用時
const runner = effect(() => console.log('effect'))
runner() // Error! this 是 undefined 或 window
notify()
執行步驟effect 的預設行為,當我們像這樣使用它時:effect(() => { ... })
資料變化 → propagate
:
當響應式資料的值被修改時,會觸發其 setter,最終由 propagate
函式開始遍歷依賴資料的 effect。
propagate
→ effect.notify()
在 propagate
的迴圈裡面,我們統一呼叫 effect.notify()
,讓它作為更新的固定入口點。
effect.notify()
→ scheduler()
notify()
內部會去呼叫 this.scheduler()
。在這個情況下,因為我們建立 effect
時沒有提供任何 options,所以 effect
實例上並不存在自己的 scheduler
屬性。
scheduler()
→ run()
根據 JavaScript 的原型鏈規則,它會回去尋找 ReactiveEffect 原型上的 scheduler()
方法。
我們預設的 scheduler()
方法,就是直接呼叫 this.run()
。因此,effect
的核心邏輯被立即執行。
資料變化 → propagate
→ effect.notify()
前三個步驟跟原形狀況完全相同:資料變更,propagate
遍歷並呼叫 effect.notify()
。
effect.notify()
→ scheduler()
notify()
內部會呼叫 this.scheduler()
。但關鍵在於,這次我們在建立 effect
時,透過 options
傳入了一個自訂的 scheduler
函式。
因此 Object.assign(e, options)
這個會把函式作為一個實例屬性附加到 effect
物件上。
scheduler()
→ 使用者的調度器邏輯今天,我們透過引入調度器,將 effect 的核心邏輯以及執行策略進行了分離,當中包含 fn
做了什麼,以及 scheduler
決定什麼時候做。
同步更新《嘿,日安!》技術部落格
但不能使用 const runner = e.run()
好奇這裡是不是要表達 const runner = e.run
,因直接 e.run()
就不是遺失 this 的問題了@@
你的理解正確,這邊我有筆誤,我修正一下寫法。
const runner = e.run
:是把函式本身存起來,呼叫之後會遺失 this,這個才是我想講的。
const runner = e.run()
:是把回傳結果存起來,這樣不能用 XD