Vue3偵聽器的實(shí)現(xiàn)原理詳情
前言:
本篇內(nèi)容基于Vue3計(jì)算屬性是如何實(shí)現(xiàn)的實(shí)現(xiàn)。
偵聽響應(yīng)式對(duì)象
前面我們聊到計(jì)算屬性,它可以自動(dòng)計(jì)算并緩存響應(yīng)式數(shù)據(jù)的值。而如果我們僅需要在響應(yīng)式數(shù)據(jù)變化時(shí),執(zhí)行一些預(yù)設(shè)的操作,就可以使用watch偵聽器。我們還是先來實(shí)現(xiàn)一個(gè)最簡單的例子,然后來一點(diǎn)一點(diǎn)擴(kuò)充它。
const data = {foo: 1}
const obj = reactive(data)
watch(obj, () => {
console.log('obj已改變')
})在這個(gè)例子中,我們使用了watch偵聽器,當(dāng)obj的屬性被改變時(shí),控制臺(tái)應(yīng)該會(huì)打印出obj已改變。基于前面我們對(duì)計(jì)算屬性的實(shí)現(xiàn),這里我們已經(jīng)有了一個(gè)大概的思路。把watch視為響應(yīng)式對(duì)象的副作用函數(shù),當(dāng)響應(yīng)式對(duì)象改變時(shí),觸發(fā)執(zhí)行該副作用函數(shù)。
想要觸發(fā)副作用函數(shù),必須先收集它,還記得副作用函數(shù)是如何收集的嗎?對(duì),當(dāng)響應(yīng)式數(shù)據(jù)被get時(shí),收集副作用函數(shù)。所以首先,我們需要讓watch被響應(yīng)式對(duì)象收集到。
function watch(getter, cb) {
effect(
() => getter.foo
)
}接著,我們還需要讓我們預(yù)設(shè)的方法被執(zhí)行。當(dāng)響應(yīng)式數(shù)據(jù)被set時(shí),觸發(fā)副作用函數(shù)。這里我們想觸發(fā)的是cb這個(gè)傳入的回調(diào)函數(shù),這里我們就又能用到實(shí)現(xiàn)計(jì)算屬性時(shí)的調(diào)度器了,當(dāng)調(diào)度器存在時(shí),set觸發(fā)的trigger會(huì)先執(zhí)行調(diào)度器中的函數(shù)。
function watch(getter, cb) {
effect(
() => getter.foo,
{
scheduler() {
cb()
}
}
)
}
一個(gè)簡單的偵聽器已經(jīng)完成了!這里我們?yōu)榱撕唵?,把功能寫死了,僅支持對(duì)obj.foo的偵聽。接下來,我們就要想想,如何實(shí)現(xiàn)對(duì)響應(yīng)式對(duì)象的任意屬性進(jìn)行偵聽?
按照前面的思路,想要實(shí)現(xiàn)對(duì)響應(yīng)式對(duì)象的任意屬性的偵聽,就需要我們get到該對(duì)象的每一個(gè)屬性,這就需要我們對(duì)響應(yīng)式對(duì)象進(jìn)行一次遞歸遍歷。
function traverse(value, seen = new Set()) { // (1)
if(typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
for(const key in value) {
traverse(value[key], seen)
}
return value
}為了避免遞歸遍歷對(duì)象時(shí),循環(huán)引用造成的死循環(huán),我們?cè)?code>(1)處創(chuàng)建了Set,當(dāng)重復(fù)出現(xiàn)相同的對(duì)象時(shí),直接返回。
偵聽屬性值
在Vue3中,我們不能直接偵聽響應(yīng)式對(duì)象的屬性值。如果需要偵聽響應(yīng)式對(duì)象的屬性值,就需要一個(gè)getter函數(shù),讓偵聽器能被響應(yīng)式對(duì)象收集到。
const data = {
foo: 1
}
const obj = reactive(data)
watch(
() => obj.foo,
() => {
console.log('obj.foo已改變')
})指定了屬性就意味著,當(dāng)前的偵聽器僅會(huì)被指定的屬性觸發(fā),就無需遞歸遍歷整個(gè)響應(yīng)式對(duì)象了。
function watch(getter, cb) {
if(typeof getter !== 'function') getter = traverse(getter) // (2)
effect(
() => getter(),
{
scheduler() {
cb()
}
}
)
}
在(2)處,我們?cè)黾恿艘粋€(gè)判斷,如果傳入的已經(jīng)是getter函數(shù),我們直接使用,如果不是getter函數(shù),則認(rèn)為是一個(gè)響應(yīng)式對(duì)象,就需要進(jìn)行遞歸遍歷。
偵聽獲取新值和舊值
在Vue中我們還需要能夠在回調(diào)函數(shù)cb()中拿到響應(yīng)式數(shù)據(jù)更新前后的新值與舊值。
const data = {
foo: 1
}
const obj = reactive(data)
watch(
() => obj.foo,
(newValue, oldValue) => {
console.log(newValue, oldValue)
})接下來的問題是,如何獲取newValue與oldValue。newValue好解決,執(zhí)行完回調(diào)函數(shù)cb()得到的就是newValue,但這里如何獲取oldValue的值呢?要從watch中拿到舊值,那就不能讓副作用函數(shù)被立即執(zhí)行。這里想到了什么?對(duì),在實(shí)現(xiàn)計(jì)算屬性的時(shí)候,我們用到過的lazy,它可以禁止副作用函數(shù)自動(dòng)執(zhí)行。
function watch(getter, cb) {
if(typeof getter !== 'function') getter = traverse(getter)
let oldValue
const effectFn = effect(
() => getter(),
{
lazy: true, // (3)
scheduler() {
cb(oldValue)
}
}
)
oldValue = effectFn() // (4)
}在(3)處我們?cè)O(shè)置了lazy開關(guān),設(shè)置了lazy后,副作用函數(shù)的執(zhí)行權(quán)就交到了我們自己手上。在(4)處,我們手動(dòng)執(zhí)行了副作用函數(shù)。這里可以需要我們向前回顧一下,前面我們傳入的getter是一個(gè)函數(shù)() => obj.foo,而effect函數(shù)的第一個(gè)參數(shù)就是真正被執(zhí)行的副作用函數(shù),所以我們手動(dòng)執(zhí)行的,其實(shí)就是函數(shù)() => obj.foo,這樣我們就拿到了舊值。
如何獲取新值呢?在響應(yīng)式數(shù)據(jù)的值更新后,副作用函數(shù)effect會(huì)被觸發(fā)執(zhí)行,當(dāng)調(diào)度器屬性存在時(shí),執(zhí)行調(diào)度器。在調(diào)度器中,我們可以再次執(zhí)行副作用函數(shù),通過() => obj.foo拿到改變后的新值。
function watch(getter, cb) {
if(typeof getter !== 'function') getter = traverse(getter)
let oldValue, newValue
const effectFn = effect(
() => getter(),
{
lazy: true,
scheduler() {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue // (5)
}
}
)
oldValue = effectFn()
}在(5)處,執(zhí)行完回調(diào)函數(shù)cb(),我們進(jìn)行了一下善后工作,更新了oldValue的值,為下一次回調(diào)做準(zhǔn)備。

有時(shí),我們還希望偵聽器可以在創(chuàng)建時(shí)就立即執(zhí)行回調(diào)函數(shù)。
const data = {
foo: 1
}
const obj = reactive(data)
watch(
() => obj.foo,
(newValue, oldValue) => {
console.log('newValue:', newValue,', oldValue:', oldValue)
},
{ immediate: true }
)當(dāng)immediate的值為true時(shí),需要立即執(zhí)行。明確了需求,我們來完善watch偵聽器。
function watch(getter, cb, options = {}) {
if(typeof getter !== 'function') getter = traverse(getter)
let oldValue, newValue
function job() { // (6)
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
const effectFn = effect(
() => getter(),
{
lazy: true,
scheduler: job,
}
)
if(options.immediate) { // (7)
job()
} else {
oldValue = effectFn()
}
}在(6)處,我們抽離了回調(diào)函數(shù)的執(zhí)行邏輯,當(dāng)options.immediate存在時(shí),直接觸發(fā)執(zhí)行。

實(shí)現(xiàn)效果

到此這篇關(guān)于Vue3偵聽器是如何實(shí)現(xiàn)的的文章就介紹到這了,更多相關(guān)Vue3偵聽器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue實(shí)現(xiàn)目錄樹結(jié)構(gòu)
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)目錄樹結(jié)構(gòu),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
vue通信方式EventBus的實(shí)現(xiàn)代碼詳解
這篇文章主要介紹了vue通信方法EventBus的實(shí)現(xiàn)代碼,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06
vue 解決mintui彈窗彈起來,底部頁面滾動(dòng)bug問題
這篇文章主要介紹了vue 解決mintui彈窗彈起來,底部頁面滾動(dòng)bug問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11
Vue如何解決子組件data從props中無法動(dòng)態(tài)更新數(shù)據(jù)問題
這篇文章主要介紹了Vue如何解決子組件data從props中無法動(dòng)態(tài)更新數(shù)據(jù)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
vue 在服務(wù)器端直接修改請(qǐng)求的接口地址
這篇文章主要介紹了vue 在服務(wù)器端直接修改請(qǐng)求的接口地址的方法,幫助大家更好的理解和使用vue,感興趣的朋友可以了解下2020-12-12
Vue3如何獲取proxy對(duì)象的值而不是引用的方式
這篇文章主要介紹了Vue3如何獲取proxy對(duì)象的值而不是引用的方式,本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-10-10
淺析webpack-bundle-analyzer在vue-cli3中的使用
這篇文章主要介紹了webpack-bundle-analyzer在vue-cli3中的使用,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10
vue2響應(yīng)式原理之Object.defineProperty()方法的使用
這篇文章主要介紹了vue2響應(yīng)式原理之Object.defineProperty()方法的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10

