面试官:请聊聊watch实现的原理?

900 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路,符合活动条件。

小白今天约了一个面试

情景对话

面试官:工作中有用到watch监听数据吗?

小白:有的,经常会监听某个属性的状态,然后去处理业务逻辑

面试官:那你知道watch底层的原理吗?

小白:就是监听响应式数据,数据改变了,就保存下之前的数据,拿到更新的数据,把这两个数据传递给回调函数就行了呗...(暗自窃喜 😏)

面试官:watch内部是怎么做的,你知道吗

小白:这。。。。再见👋 👋

watch的用法

在vue3中,watch可以这样使用,这里贴上官网用法:

image.png

今天我们主要聊聊watch,如何监听一个响应式对象和一个getter函数

实现

监听getter函数

假如我们有一个代理对象newObj,调用watch函数,去实现监听,示例如下

const obj={
    name:"张三",
    flag:true,
    count:1,

}
//代理对象newObj,相当于 const newObj=reactive({ ...})
const newObj=new Proxy(obj,{
    get(){
        ...
    },
    set(){
        ...
    }
})
watch(()=>newObj.name,(newValue,oldValue)=>{
    console.log("旧值是"+oldValue);
    console.log("新值是"+newValue);

})
newObj.name="李四"

接下来,我们思考下,我么要如何实现一个watch呢?

1.思路

1.读取newObj.name的值,使用track收集依赖

2.每当数据改变的时候,不能立马通过trigger触发依赖,而是要在合适的时机去触发依赖,这里我们就想到了调度函数

请戳 :track收集依赖和trigger收集依赖

请戳 :vue设计与实现-调度器

粗略的实现一下

function watch(getter,cb){
    let oldValue,newValue
    const effectFn= effect(getter,{
        lazy:true,
        schedule(fn){
             newValue=fn()
             cb(newValue,oldValue)
             oldValue=newValue
        }
    })
    oldValue= effectFn()
}

上述代码逻辑:

1.lazy懒加载执行effectFn,因为watch是惰性的,将name值首先赋值给oldValue,此时track收集及依赖;

2.当我们修改name属性的值时,会触发trigger函数,然后发现有调度器,走调度器的逻辑,这个时候,通过执行fn将name最新的值赋值给newValue

3.然后再执行用户传进来的回调函数cb,此时把newValue和oldValue作为参数传入

4.最后一步,重新给oldValue赋值,这样可以保证下次,name更新的时候,oldValue拿的是最新的值

测试一下,没问题!yeah

image.png

监听一个响应式对象

1.思路

有了上面的getter的实现,我们想监听一个对象,似乎并不难 思路大概是: 1.拿到一个对象,遍历读取一下,循环执行track函数读取

2.在修改对象的某一个属性的时候,拿到修改后的对象给到newValue,保存修改前的对象给到oldValue

2.实现

//调用
watch(newObj,()=>{
    console.log("旧值是");
    console.log(oldValue);
    console.log("新值是");
    console.log(newValue);

})
newObj.name="李四"

在之前的watch函数上进行了修改

function traverse (value,seen=new Set()){
    if(typeof value !=='object'||value==null||seen.has(value)) return
    seen.add(value)
    for (const key in value) {
        traverse(value[key],seen) 
    }
    return value

}
function watch(source,cb){
    let getter
    if(typeof source =='function'){
        getter=source
    }else {
        getter=()=>traverse(source)
    }
    let oldValue,newValue
    const effectFn=effect(getter,{
        lazy:true,
        schedule(fn){
            newValue=fn()
            cb(newValue,oldValue)
            oldValue=newValue
        }
    })
    oldValue=effectFn()
    
}


上面的代码:

1.traverse函数 循环读取对象的属性,进行track

2将traverse函数的返回值作为一个getter传入effect,下面的逻辑就和getter相似了

当我们执行代码时:

image.png

咦?啥情况,为啥会这样?🤔 🤔oldValue也被修改了 我们不妨再修改一下试试

newObj.name="王武"

效果如下:

image.png

看到这里,诸位小伙伴应该已经看到问题所在了吧?😁 😁

其实,原理很简单,对象赋值---是引用地址 比如:

let a={
    x:1
}
let b
b=a
a.x=2
console.log(b);

image.png

同理,这里也是一样的,我们简单粗暴的修改一下

function watch(source,cb){
    let getter
    if(typeof source =='function'){
        getter=source
    }else {
        getter=()=>traverse(source)
    }
    let oldValue,newValue
    const effectFn=effect(getter,{
        lazy:true,
        schedule(fn){
        //一通乱改🤔
            newValue=JSON.parse(JSON.stringify(fn()))
            cb(newValue,oldValue)
            oldValue=JSON.parse(JSON.stringify(newValue))
        }
    })
    let currentValue= effectFn()
    //一通乱改🤔
    if(typeof currentValue=='object'){
        oldValue= JSON.parse(JSON.stringify(currentValue))
    }else{
        oldValue=currentValue
    }
    
}

效果如下:

image.png

实现了!

当然,目前实现的watch并不是最优的方案,watch也还有很多功能点,比如,对数组的监听等等,欢迎各位大佬留言,提出意见~ 感谢🙏🙏