
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 參數的實作。
同步更新《嘿,日安!》技術部落格