watch 是 Vue 非常重要的一個 API,它允許開發者在響應式資料發生變化時,執行特定的副作用(side effects)。這些副作用可以是異步行為,像是發起請求,也可以是需要基於狀態變化執行的複雜邏輯。
在實作之前,我們先來回憶在實作 effect
的時候,我們有做一個 Scheduler 調度器,然而watch
的核心原理與 effect
的調度器(Scheduler)密切相關。
調度器的設計目標是:當響應式數據變更時,不直接重新執行 effect
的主體函式,而是執行一個指定的調度函式。
細節可以回去看之前寫的文章。
watch
本質上是 effect
的一種應用。它利用了調度器機制,來實現『監聽資料變更,並執行指定 callback 函式』的功能。
接收參數:
deep
、immediate
、once
返回值:一個函式,主要目的是停止監聽
我們建立一個 watch.ts
檔案,並且導出。
在實作 watch
時,我們直接使用 ReactiveEffect
類別,而不是 effect
函式。
主要原因是 effect
函式返回的是 runner
,我們無法直接取得內部 fn
的返回值,但如果直接使用 ReactiveEffect
實例,可以通過呼叫 effect.run()
來取得返回值。
export function effect(fn, options) {
const e = new ReactiveEffect(fn)
Object.assign(e, options)
e.run()
const runner = e.run.bind(e)
runner.effect = e
return runner <= 沒有 fn 返回值
}
然而ReactiveEffect
類別需要傳入一個函式,但是 source 參數不一定是函式,他有可能是一個 ref 物件,因此一開始我們利用 getter 包裝成一個函式。
import { isRef } from './ref'
import { ReactiveEffect } from './effect'
export function watch(source, cb, options) {
let getter // 做成函式 傳入 effect
if(isRef(source)) { // source 有可能是 ref 物件,進行函式的包裝
getter = () => source.value
}
/**
* 使用 effect 類別,而不使用 effect 函式,是因為 effect 沒有返回 effect.run() 返回值
*/
const effect = new ReactiveEffect(getter) //effect 要接收一個函式
}
接下來,我們需要定義 job
函式,它將作為 effect
的調度器。當監聽的資料發生改變時,job
函式會被觸發,主要功能如下:
effect.run()
,這會重新執行 getter
並返回最新的值(newValue
)。cb(newValue, oldValue)
。newValue
賦給 oldValue
,為下一次變更做準備。import { isRef } from './ref'
import { ReactiveEffect } from './effect'
export function watch(source, cb, options) {
let getter // 做成函式 傳入 effect
if(isRef(source)) { // source 有可能是 ref 物件,進行函式的包裝
getter = () => source.value
}
let oldValue
function job() {
// 執行 effect 的函式,得到新的數值,不能直接執行 getter,因為要收集依賴
const newValue = effect.run()
cb(newValue, oldValue)
// 這次更新的新數值就是下次的舊數值
oldValue = newValue
}
/**
* 使用 effect 類別,而不使用 effect 函式,是因為 effect 沒有返回 effect.run() 返回值
*/
const effect = new ReactiveEffect(getter) //effect 要接收一個函式
effect.scheduler = job
oldValue = effect.run() // 收集依賴後,得到 run 返回值,取得舊的數值
return () => {} // 停止監聽
}
我們實作看看 index.html
<body>
<div id="app"></div>
<script type="module">
// import { ref, computed, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { ref, watch } from '../dist/reactivity.esm.js'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log('newVal, oldVal', newVal, oldVal)
})
setTimeout(() => {
count.value = 1
}, 1000)
</script>
</body>
watch
建立一個內部的 effect
來監聽 count
effect
會立即執行一次,主要目的有兩個:
count.value
,讓 watch
開始追蹤 count
的後續變化。count
的當前值 0
,並將其存放在 watch
函式內部的 oldValue
變數中。console.log
在這個階段不會被執行。setTimeout
執行)count.value
的值被更新為 1
。watch
內部建立的 effect
,但它執行的是我們自訂的調度器 (scheduler
)。scheduler
已經被賦予成 job
,直接呼叫 job
函式。job
內部會:
effect.run()
來取得 count
的新值 1
。(newValue: 1, oldValue: 0)
。console.log
因此印出 newVal, oldVal 1 0
。1
存在 oldValue
,確保下次更新oldValue
是正確的舊值。停止監聽函式剛剛我們沒有寫,接著完成。
<body>
<div id="app"></div>
<script type="module">
// import { ref, watch, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { ref, watch } from '../dist/reactivity.esm.js'
const count = ref(0)
const stop = watch(count, (newVal, oldVal) => {
console.log('newVal, oldVal', newVal, oldVal)
})
setTimeout(() => {
count.value = 1
setTimeout(() => {
stop()
count.value = 2
}, 1000)
}, 1000)
</script>
</body>
這個範例會輸出兩次,如果我們希望不要輸出第二次結果,應該怎麼做?
首先我們先做一個 active
的監聽標記:
run
方法中,如果沒有監聽標記,我們就返回 fn 的返回值stop
停止監聽方法:
stop
方法的核心是清除 effect
實例上收集到的所有依賴。這可以通過組合使用 startTrack
和 endTrack
來實現:startTrack(this)
,此操作會重置 effect
內部的依賴追蹤指針。endTrack(this)
,此操作會清除從指針當前位置到依賴列表末尾的所有依賴項。effect
的全部依賴。最後,通過將 active
標記設為 false
,可以阻止 effect
後續被意外重新執行。effect.ts
export class ReactiveEffect implements Sub {
active = true // 是否啟動監聽
...
...
}
run() {
if(!this.active) {
return this.fn()
}
...
...
}
...
...
stop() {
// 停止監聽
if(this.active) {
startTrack(this)
endTrack(this)
this.active = false
}
}
}
接著寫一個 stop
做返回函式。
watch.ts
export function watch(source, cb, options) {
...
...
function stop() {
effect.stop()
}
return () => {
stop()
}
}
這樣子就會輸出一次。
總結來說,我們透過直接利用 ReactiveEffect
類別及其調度器(Scheduler)功能,完成了基礎的 watch
實作。
關鍵在於透過 job
函式攔截更新通知,並在其中執行 effect.run()
以取得新舊值,最終呼叫使用者 callback。
同時,我們也為其增加了 stop
方法,實現了手動停止監聽的功能。
下一篇我們會探討 watch
的 option
參數的實作。
同步更新《嘿,日安!》技術部落格