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()
          }
        }
      }
    )

  }
相关推荐
手机忘记时间17 分钟前
在R语言中如何将列的名字改成别的
java·前端·python
苹果酱056718 分钟前
[数据库之十一] 数据库索引之联合索引
java·vue.js·spring boot·mysql·课程设计
郝郝先生--34 分钟前
Flutter 异步原理-Zone
前端·flutter
geovindu1 小时前
vue3: pdf.js5.2.133 using typescript
javascript·vue.js·typescript·pdf
花开花落的博客1 小时前
uniapp 不同路由之间的区别
前端·uni-app
whatever who cares1 小时前
React 中 useMemo 和 useEffect 的区别(计算与监听方面)
前端·javascript·react.js
老兵发新帖1 小时前
前端知识-hook
前端·react.js·前端框架
t_hj1 小时前
Ajax的原理和解析
前端·javascript·ajax
蓝婷儿2 小时前
前端面试每日三题 - Day 29
前端·面试·职场和发展
小白上线*^_^*2 小时前
Vue——Axios
前端·javascript·vue.js