• 【拆解Vue3】侦听器是如何实现的?


    侦听响应式对象

    前面我们聊到计算属性,它可以自动计算并缓存响应式数据的值。而如果我们仅需要在响应式数据变化时,执行一些预设的操作,就可以使用watch侦听器。我们还是先来实现一个最简单的例子,然后来一点一点扩充它。

    const data = {foo: 1}
    const obj = reactive(data)
    watch(obj, () => {console.log('obj已改变')
    }) 
    

    在这个例子中,我们使用了watch侦听器,当obj的属性被改变时,控制台应该会打印出obj已改变。基于前面我们对计算属性的实现,这里我们已经有了一个大概的思路。watch视为响应式对象的副作用函数,当响应式对象改变时,触发执行该副作用函数

    想要触发副作用函数,必须先收集它,还记得副作用函数是如何收集的吗?对,当响应式数据被get时,收集副作用函数。所以首先,我们需要让watch被响应式对象收集到。

    function watch(getter, cb) {effect(() => getter.foo)
    } 
    

    接着,我们还需要让我们预设的方法被执行。当响应式数据被set时,触发副作用函数。这里我们想触发的是cb这个传入的回调函数,这里我们就又能用到实现计算属性时的调度器了,当调度器存在时,set触发的trigger会先执行调度器中的函数。

    function watch(getter, cb) {effect(() => getter.foo,{scheduler() {cb()}})
    } 
    

    一个简单的侦听器已经完成了!这里我们为了简单,把功能写死了,仅支持对obj.foo的侦听。接下来,我们就要想想,如何实现对响应式对象的任意属性进行侦听?

    按照前面的思路,想要实现对响应式对象的任意属性的侦听,就需要我们get到该对象的每一个属性,这就需要我们对响应式对象进行一次递归遍历。

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

    为了避免递归遍历对象时,循环引用造成的死循环,我们在(1)处创建了Set,当重复出现相同的对象时,直接返回。

    侦听属性值

    在Vue3中,我们不能直接侦听响应式对象的属性值。如果需要侦听响应式对象的属性值,就需要一个getter函数,让侦听器能被响应式对象收集到。

    const data = {foo: 1
    }
    const obj = reactive(data)
    watch(() => obj.foo, () => {console.log('obj.foo已改变')
    }) 
    

    指定了属性就意味着,当前的侦听器仅会被指定的属性触发,就无需递归遍历整个响应式对象了。

    function watch(getter, cb) {if(typeof getter !== 'function') getter = traverse(getter) // (2)effect(() => getter(),{scheduler() {cb()}})
    } 
    

    在(2)处,我们增加了一个判断,如果传入的已经是getter函数,我们直接使用,如果不是getter函数,则认为是一个响应式对象,就需要进行递归遍历。

    侦听获取新值和旧值

    Vue中我们还需要能够在回调函数cb()中拿到响应式数据更新前后的新值与旧值。

    const data = {foo: 1
    }
    const obj = reactive(data)
    watch(() => obj.foo, (newValue, oldValue) => {console.log(newValue, oldValue)
    }) 
    

    接下来的问题是,如何获取newValueoldValuenewValue好解决,执行完回调函数cb()得到的就是newValue,但这里如何获取oldValue的值呢?要从watch中拿到旧值,那就不能让副作用函数被立即执行。这里想到了什么?对,在实现计算属性的时候,我们用到过的lazy,它可以禁止副作用函数自动执行。

    function watch(getter, cb) {if(typeof getter !== 'function') getter = traverse(getter)let oldValueconst effectFn = effect(() => getter(),{lazy: true, // (3)scheduler() {cb(oldValue)}})oldValue = effectFn() // (4)
    } 
    

    在(3)处我们设置了lazy开关,设置了lazy后,副作用函数的执行权就交到了我们自己手上。在(4)处,我们手动执行了副作用函数。这里可以需要我们向前回顾一下,前面我们传入的getter是一个函数() => obj.foo,而effect函数的第一个参数就是真正被执行的副作用函数,所以我们手动执行的,其实就是函数() => obj.foo,这样我们就拿到了旧值。

    如何获取新值呢?在响应式数据的值更新后,副作用函数effect会被触发执行,当调度器属性存在时,执行调度器。在调度器中,我们可以再次执行副作用函数,通过() => obj.foo拿到改变后的新值。

    function watch(getter, cb) {if(typeof getter !== 'function') getter = traverse(getter)let oldValue, newValueconst effectFn = effect(() => getter(),{lazy: true,scheduler() {newValue = effectFn()cb(newValue, oldValue)oldValue = newValue // (5)}})oldValue = effectFn()
    } 
    

    在(5)处,执行完回调函数cb(),我们进行了一下善后工作,更新了oldValue的值,为下一次回调做准备。

    有时,我们还希望侦听器可以在创建时就立即执行回调函数。

    const data = {foo: 1
    }
    const obj = reactive(data)
    watch(() => obj.foo, (newValue, oldValue) => {console.log('newValue:', newValue,', oldValue:', oldValue)},{ immediate: true }
    ) 
    

    immediate的值为true时,需要立即执行。明确了需求,我们来完善watch侦听器。

    function watch(getter, cb, options = {}) {if(typeof getter !== 'function') getter = traverse(getter)let oldValue, newValuefunction 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)处,我们抽离了回调函数的执行逻辑,当options.immediate存在时,直接触发执行。

    最后,最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



    有需要的小伙伴,可以点击下方卡片领取,无偿分享

  • 相关阅读:
    昇思25天学习打卡营第3天|onereal
    深入剖析Tomcat之ERR_INVALID_HTTP_RESPONSE
    开放 LLM 排行榜: 深入研究 DROP
    flutter系列之:移动端的手势基础GestureDetector
    声学回声消除原理
    android利用FFmpeg进行视频转换
    MyBatis 执行原理,源码解读,基于SpringBoot讲解
    Endnote X9文献管理器应用---使用总结
    GBase 8a 扩容操作意外的处理方案
    推荐动漫《头文字D》
  • 原文地址:https://blog.csdn.net/pfourfire/article/details/126957611