• 【源码系列#04】Vue3侦听器原理(Watch)


    专栏分享: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
      },
    )

    数据源

    1. 数据源为 ref 的情况,和 immediate 回调执行时机
    const x = ref(1)
    
    watch(
      x,
      (newValue, oldValue) => {
        console.log('x', newValue, oldValue)
      }, {
        immediate: true
      },
    )
    
    setTimeout(() => {
      x.value = 2
    }, 100)
    1. 兼容数据源为 响应式对象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)

    __EOF__

  • 本文作者: 柏成
  • 本文链接: https://www.cnblogs.com/burc/p/17916475.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    精酿啤酒:酿造过程中的设备选择与效率提升
    经典网络解析(四) ResNet | 残差模块,网络结构代码实现全解析
    6.14-IPv6 6.15-IPv4与IPv6网络通信 6.16-路由
    行业追踪,2023-09-15
    代码随想录算法训练营第六十三天 |84.柱状图中最大的矩形
    餐饮行业软文怎么写吸引人?怎么打动人心
    Spring IOC之ApplicationContextAware
    Leetcode 101. 对称二叉树
    【HTML】笔记2-表格、列表、表单标签
    【JVM】G1垃圾收集器
  • 原文地址:https://www.cnblogs.com/burc/p/17916475.html