前置知識點:
執行排程 https://segmentfault.com/a/11...
計算屬性 https://segmentfault.com/a/11...
正文
在上一篇介紹了計算屬性的實現原理, 這篇是 Vue 中時常和計算屬性做比較的偵聽器的原理實現的簡介.
所謂的偵聽器watch
本質上就是觀測響應式資料是否發生變化, 當資料發生變化時通知並執行相應的回撥函式:
watch (obj, () => {
console.log('資料變化了')
})
// 修改資料導致響應式資料變化
obj.foo++
本質上是利用了副作用函式effect
以及排程選項option.scheduler
:
effect(() => {
console.log(obj.foo)
},
// options
{
scheduler () {
// obj.foo 變化時, 執行 scheduler 排程函式
}
})
如果副作用函式存在scheduler
選項, 當響應式資料發生變化時會觸發scheduler
排程函式執行, 而不是直接觸發副作用函式利用這點可以實現最簡單的watch
函式:
function watch (source, cb) {
effect(
// 觸發讀取操作, 從而建立聯絡
() => source.foo,
{
scheduler () {
// obj.foo 變化時, 執行 scheduler 排程函式
cb()
}
}
)
}
但是這個太基本了, 還只能監聽obj.foo
這個屬性的變化, 因此需要封裝一個通用的讀取操作, 使watch
具有通用性:
function watch (source, cb) {
effect(
// 觸發讀取操作, 從而建立聯絡
// 呼叫函式遞迴讀取將每一個資料都建立聯絡
() => traverse(source),
{
scheduler () {
// obj.foo 變化時, 執行 scheduler 排程函式
cb()
}
}
)
}
function traverse (value, seen = new Set()) {
// 如果該值是原始資料型別, 或者已被讀取過來就什麼都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
// 將資料新增到 seen 中, 代表遍歷過了 避免引起死迴圈
seen.add(value)
// 假設 value就是就是一個物件, 暫時不考慮陣列等情況
for (const k in value) {
// 遞迴呼叫 traverse
traverse(value[k], seen)
}
return value
}
這樣就可以讀取物件上的任意屬性, 從而當任意屬性發生變化時都能觸發回撥函式執行, watch
不僅僅可以觀測響應函式還可以接受getter
函式:
watch (
// getter 函式
() => obj.foo,
// 回撥函式
() => console.log('obj.foo 的值改變了')
)
在getter
函式的內部可以指定改watch
依賴哪些響應式資料, 只有當這些資料變化時才會觸發回撥函式執行:
function watch (source, cn) {
// 定義 getter
let getter
// 如果 source 是函式, 說明使用者 傳遞的是 getter
if (typeof source === 'function') {
getter = source
} else {
// 否則按照原來的呼叫 traverse 遞迴讀取
getter = () => traverse(source)
}
effect(
// 執行 getter 獲取值
() => getter(),
{
scheduler () {
// obj.foo 變化時, 執行 scheduler 排程函式
cb()
}
}
)
}
這時功能已經比價完善了, 不過目前還少了一點就是不能夠得到變化前後的值, 但是在 Vue.js 中是可以的:
watch (
// getter 函式
() => obj.foo,
// 回撥函式
(newVal, oldVal) => console.log(newVal, oldVal)
)
因此需要獲取新值與舊值, 這時可以利用effect
的lazy
選項:
關於 lazy 請參看: https://segmentfault.com/a/11...
function watch (source, cn) {
// 定義 getter
let getter
// 如果 source 是函式, 說明使用者 傳遞的是 getter
if (typeof source === 'function') {
getter = source
} else {
// 否則按照原來的呼叫 traverse 遞迴讀取
getter = () => traverse(source)
}
let oldValue, newValue
// 使用 effect 註冊副作用函式時開啟 lazy 選項, 並把返回的值儲存到 effectFn 中以便後續手動呼叫
const effectFn = effect(
// 執行 getter 獲取值
() => getter(),
{
lazy: true,
scheduler () {
// 重新執行 effectFn 得到的是新值
newValue = effectFn()
// 將新舊值作為回撥函式的引數
// obj.foo 變化時, 執行 scheduler 排程函式
cb(newValue, oldValue)
// 更新舊值, 不然下一次就會得到錯誤的舊值
oldValue = newValue
}
}
)
// 手動呼叫副作用函式拿到的就是舊值
oldValue = effectFn()
}
在程式碼的最下面, 手動呼叫 effectFn 函式返回的值得到的就是舊值也就是第一次執行得到的值, 當觸發 scheduler 排程函式時會重新呼叫 effectFn 得到新值.