09.vue3中watch实现思路

1.watch的基础概念

所谓 watch,其本质就是观测一个响应式数据,当数据发生变化 时通知并执行相应的回调函数。举个例子

js 复制代码
 watch(obj, () => {
     console.log('数据变了')
 })
 // 修改响应数据的值,会导致回调函数执行
 obj.foo++

假设 obj 是一个响应数据,使用 watch 函数观测它,并传递一个 回调函数,当修改响应式数据的值时,会触发该回调函数执行。

2.watch的简单实现

实际上,watch 的实现本质上就是利用了 effect 以及 options.scheduler 选项,如以下代码所示:

js 复制代码
effect(
  () => {
   console.log(obj.foo)
   }, 
  {
   scheduler() {
   // 当 obj.foo 的值变化时,会执行 scheduler 调度函数
   }
})

那我们知道上面的代码,那我们就可以实现一个最简单的watch,代码如下:

js 复制代码
// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数
 function watch(source, cb) {
   effect(
   // 触发读取操作,从而建立联系
     () => source.foo,
     {
       scheduler() {
         // 当数据变化时,调用回调函数 cb
           cb()
       }
   })
 }

通过上述,我们就实现了一个最简单的watch,但是我们使通过硬编码的方式,直接读取了foo这个属性,那我们能不能让watch更加具有通用性,那我们就需要封装一个通用的读取操作

js 复制代码
  function watch(source, cb, options = {}) {
    effect(
      () => traverse(source),
      {
        scheduler() {
          cb()
        }
      }
    )
  }

  function traverse(value, seen = new Set()) {
    if (typeof value !== 'object' || value === null || seen.has(value)) return
    seen.add(value)
    // 遍历对象的每个属性,暂不考虑数组格式
    for (const k in value) {
      traverse(value[k], seen)
    }
    return value
  }

如上面的代码所示,在 watch 内部的 effect 中调用 traverse 函数进行递归的读取操作,代替硬编码的方式,这样就能读取一个对 象上的任意属性,从而当任意属性发生变化时都能够触发回调函数执行。

3.watch中接受getter函数的处理

watch 函数除了可以观测响应式数据,还可以接收一个 getter 函数:

js 复制代码
watch(
 // getter 函数
   () => obj.foo,
   // 回调函数
   () => {
     console.log('obj.foo 的值变了')
   }
)

如以上代码所示,传递给 watch 函数的第一个参数不再是一个响应式数据,而是一个 getter 函数。在 getter 函数内部,用户可以指定该 watch 依赖哪些响应式数据,只有当这些数据变化时,才会触发回调函数执行。

那我们的代码就需要做如下改造

js 复制代码
 function watch(source, cb, options = {}) {
    let getter
    if (typeof source === 'function') {
      getter = source
    } else {
      getter = () => traverse(source)
    }
   effect(
      () => getter(),
      {
        scheduler() {
          cb()
        }
      }
    )
 }

那我们就处理了传递对象和对getter函数的处理了,那我们接下来就处理,再cb函数中应该拿到newValue和oldValue的问题了

js 复制代码
  function watch(source, cb, options = {}) {
    let getter
    if (typeof source === 'function') {
      getter = source
    } else {
      getter = () => traverse(source)
    }
    let oldValue, newValue
    // 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
    const effectFn = effect(
      () => getter(),
      {
        lazy: true,
        scheduler() {
          newValue = effectFn()
          cb(newVlaue, oldValue)
          oldValue = newValue
        }
      }
    )
    //手动调用副作用函数,拿到的值就是旧值
    oldValue = effectFn()
  }

我们手动调用 effectFn 函数得到的返回值就是旧值,即第一次执行得到的值。当变化发生并触发 scheduler 调度函数执行时,会重新调用 effectFn 函数并得到新值,这样我们就拿到了旧值与新值。

4.watch的立即执行时机

当 immediate 选项存在并且为 true 时,回调函数会在该 watch 创建时立刻执行一次。那我们简单思考一下,回调稍后执行和立即执行没什么区别,那我们只需要把函数封装一下,再初始化和变更时执行就行了

js 复制代码
  function watch(source, cb, options = {}) {
    let getter
    if (typeof source === 'function') {
      getter = source
    } else {
      getter = () => traverse(source)
    }

    let oldValue, newValue

    const job = () => {
      newValue = effectFn()
      cb(oldValue, newValue)
      oldValue = newValue
    }

    const effectFn = effect(
      // 执行 getter
      () => getter(),
      {
        lazy: true,
        scheduler: () => {
          job()
        }
      }
    )

    if (options.immediate) {
      job()
    } else {
      oldValue = effectFn()
    }
  }

这样,那我们就实现了watch的立即执行了。

5.回调时间的触发

默认情况下,侦听器回调会在父组件更新 (如有) 之后 、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。

如果想在侦听器回调中能访问被 Vue 更新之后 的所属组件的 DOM,你需要指明 flush: 'post' 选项:

flush 本质上是在指定调度函数的执行时机。前文讲解过如何在 微任务队列中执行调度函数 scheduler,这与 flush 的功能相同。 当 flush 的值为 'post' 时,代表调度函数需要将副作用函数放到一 个微任务队列中,并等待 DOM 更新结束后再执行,我们可以用如下代 码进行模拟:

js 复制代码
 function watch(source, cb, options = {}) {
    let getter
    const effectFn = effect(
      // 执行 getter
      () => getter(),
      {
        lazy: true,
        scheduler: () => {
          if (options.flush === 'post') { //新增代码
            const p = Promise.resolve()
            p.then(job)
          } else {
            job()
          }
        }
      }
    )

  }
相关推荐
刘_小_二18 分钟前
Nginx设置开机自启动(Linux版本)
前端
小鱼计算机25 分钟前
【6】深入学习http模块(万字)-Nodejs开发入门
前端·javascript·http·node.js·http请求
再玩一会儿看代码27 分钟前
彻底掌握 XMLHttpRequest(XHR):前端通信的基石
前端·经验分享·笔记·学习·xhr
爱上大树的小猪29 分钟前
【前端样式】使用Flexbox实现经典导航栏:自适应间距与移动端折叠实战
前端·css·面试
前端开发呀31 分钟前
无所不能的uniapp拦截器
前端·uni-app
itsOli37 分钟前
(22)详情页开发——④ 详情页“列表”和“用户评论”组件 | Vue.js 项目实战: 移动端“旅游网站”开发
前端·javascript·vue.js
前端大卫1 小时前
mac 常用技巧与问题汇总
前端·mac
鸿蒙场景化示例代码技术工程师1 小时前
实现页面全屏功能鸿蒙示例代码
前端
funnycoding1 小时前
mcp vs function call区别
前端·架构
鸿蒙场景化示例代码技术工程师1 小时前
实现PdfKit功能鸿蒙示例代码
前端