專欄分享:vue2原始碼專欄,vue3原始碼專欄,vue router原始碼專欄,玩具專案專欄,硬核💪推薦🙌
歡迎各位ITer關注點贊收藏🌸🌸🌸
語法
偵聽一個或多個響應式資料來源,並在資料來源變化時呼叫所給的回撥函式
const x = ref(0)
const y = ref(0)
// 單個 ref
watch(x, (newValue, oldValue) => {
console.log(`x is ${newValue}`)
})
// getter 函式
watch(
() => x.value + y.value,
(newValue, oldValue) => {
console.log(`sum of x + y is: ${newValue}`)
}
)
// 多個來源組成的陣列
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
第一個引數可以是不同形式的“資料來源”:它可以是一個 ref (包括計算屬性)、一個響應式物件、一個 getter 函式、或多個資料來源組成的陣列
第二個引數是在發生變化時要呼叫的回撥函式。這個回撥函式接受三個引數:新值、舊值,以及一個用於註冊副作用清理的回撥函式。該回撥函式會在副作用下一次重新執行前呼叫,可以用來清除無效的副作用,例如等待中的非同步請求。
第三個可選的引數是一個物件,支援以下這些選項:
- immediate:在偵聽器建立時立即觸發回撥。第一次呼叫時舊值是 undefined。
- deep:如果源是物件,強制深度遍歷,以便在深層級變更時觸發回撥。參考深層偵聽器。
- flush:調整回撥函式的重新整理時機。參考回撥的重新整理時機及 watchEffect()。
- onTrack / onTrigger:除錯偵聽器的依賴。參考除錯偵聽器。
原始碼實現
-
@issue1 深度遞迴迴圈時考慮物件中有迴圈引用的問題
-
@issue2 相容資料來源為響應式物件和getter函式的情況
-
@issue3 immediate回撥執行時機
-
@issue4 onCleanup該回撥函式會在副作用下一次重新執行前呼叫
/**
* @desc 遞迴迴圈讀取資料
* @issue1 考慮物件中有迴圈引用的問題
*/
function traversal(value, set = new Set()) {
// 第一步遞迴要有終結條件,不是物件就不在遞迴了
if (!isObject(value)) return value
// @issue1 處理迴圈引用
if (set.has(value)) {
return value
}
set.add(value)
for (let key in value) {
traversal(value[key], set)
}
return value
}
/**
* @desc watch
* @issue2 相容資料來源為響應式物件和getter函式的情況
* @issue3 immediate 立即執行
* @issue4 onCleanup:用於註冊副作用清理的回撥函式。該回撥函式會在副作用下一次重新執行前呼叫,可以用來清除無效的副作用,例如等待中的非同步請求
*/
// source 是使用者傳入的物件, cb 就是對應的回撥
export function watch(source, cb, { immediate } = {} as any) {
let getter
// @issue2
// 是響應式資料
if (isReactive(source)) {
// 遞迴迴圈,只要迴圈就會訪問物件上的每一個屬性,訪問屬性的時候會收集effect
getter = () => traversal(source)
} else if (isRef(source)) {
getter = () => source.value
} else if (isFunction(source)) {
getter = source
}else {
return
}
// 儲存使用者的函式
let cleanup
const onCleanup = fn => {
cleanup = fn
}
let oldValue
const scheduler = () => {
// @issue4 下一次watch開始觸發上一次watch的清理
if (cleanup) cleanup()
const newValue = effect.run()
cb(newValue, oldValue, onCleanup)
oldValue = newValue
}
// 在effect中訪問屬性就會依賴收集
const effect = new ReactiveEffect(getter, scheduler) // 監控自己構造的函式,變化後重新執行scheduler
// @issue3
if (immediate) {
// 需要立即執行,則立刻執行任務
scheduler()
}
// 執行getter,讓getter中的每一個響應式變數都收集這個effect
oldValue = effect.run()
}
測試程式碼
迴圈引用
物件中存在迴圈引用的情況
const person = reactive({
name: '柏成',
age: 25,
address: {
province: '山東省',
city: '濟南市',
}
})
person.self = person
watch(
person,
(newValue, oldValue) => {
console.log('person', newValue, oldValue)
}, {
immediate: true
},
)
資料來源
- 資料來源為 ref 的情況,和 immediate 回撥執行時機
const x = ref(1)
watch(
x,
(newValue, oldValue) => {
console.log('x', newValue, oldValue)
}, {
immediate: true
},
)
setTimeout(() => {
x.value = 2
}, 100)
- 相容資料來源為 響應式物件 和 getter函式 的情況,和 immediate 回撥執行時機
const person = reactive({
name: '柏成',
age: 25,
address: {
province: '山東省',
city: '濟南市',
}
})
// person.address 物件本身及其內部每一個屬性 都收集了effect。traversal遞迴遍歷
watch(
person.address,
(newValue, oldValue) => {
console.log('person.address', newValue, oldValue)
}, {
immediate: true
},
)
// 注意!我們在 watch 原始碼內部滿足了 isFunction 條件
// 此時只有 address 物件本身收集了effect,僅當 address 物件整體被替換時,才會觸發回撥;
// 其內部屬性發生變化並不會觸發回撥
watch(
() => person.address,
(newValue, oldValue) => {
console.log('person.address', newValue, oldValue)
}, {
immediate: true
},
)
// person.address.city 收集了 effect
watch(
() => person.address.city,
(newValue, oldValue) => {
console.log('person.address.city', newValue, oldValue)
}, {
immediate: true
},
)
setTimeout(() => {
person.address.city = '青島市'
}, 100)
onCleanup
watch回撥函式接受三個引數:新值、舊值,以及一個用於註冊副作用清理的回撥函式(即我們的onCleanup)。該回撥函式會在副作用下一次重新執行前呼叫,可以用來清除無效的副作用,例如等待中的非同步請求。
const person = reactive({
name: '柏成',
age: 25
})
let timer = 3000
function getData(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(timer)
}, timer)
})
}
// 1. 第一次呼叫watch的時候注入一個取消的回撥
// 2. 第二次呼叫watch的時候會執行上一次注入的回撥
// 3. 第三次呼叫watch會執行第二次注入的回撥
// 後面的watch觸發會將上次watch中的 clear 置為true
watch(
() => person.age,
async (newValue, oldValue, onCleanup) => {
let clear = false
onCleanup(() => {
clear = true
})
timer -= 1000
let res = await getData(timer) // 第一次執行2s後渲染2000, 第二次執行1s後渲染1000, 最終應該是1000
if (!clear) {
document.body.innerHTML = res
}
},
)
person.age = 26
setTimeout(() => {
person.age = 27
}, 0)