侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

vue3源码学习-8-watch

蜗牛
2022-06-19 / 0 评论 / 0 点赞 / 15 阅读 / 7875 字 / 正在检测是否收录...

前言

本文学习编写watch功能函数。首先,先去使用下官方的watch做一些简单的小功能测试。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>computed</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- 官方的 -->
    <script src="../../node_modules/vue/dist/vue.global.js"></script>
    <!-- <script src="./dist/reactivity.global.js"></script> -->
    <script>
      // effect 代表的是副作用函数,如果函数依赖发生改变,他就重新执行
      // reactive 将数据变成响应式 相当于proxy
      // shallowRactive,readonly,shallowReadonly
      const { reactive, watch } = Vue
      const state = reactive({ name: 'jw', address: { num: 1 } })
      watch(
        state,
        (newValue, oldValue) => {
          console.log(newValue, oldValue) // 数据改变触发了打印
        }
      )

      watch(
        () => state.address.num,
        (newValue, oldValue) => {
          console.log(newValue, oldValue) // 数据改变触发了打印
        }
      )

      watch(
        state.address.num,
        (newValue, oldValue) => {
          console.log(newValue, oldValue) //  没触发打印
        }
      )

      setTimeout(() => {
        state.address.num = 123
      }, 1000)
    </script>
  </body>
</html>

上面尝试了3中方式,结果发现第三种不触发打印,因为你监听的是数值1,一个常量就不会有改变的情况。
另外如果你监听一个对象,例如state.address这样的,那也不会触发打印。因为这个对象是引用类型,也就是在内存中的地址也没改变,你就是改变了对象属性的值,他监听不到改变。
第二种写法是比较优的方式。

编写watch

在上述官方的使用过程中,watch接受了2个参数。

  • 用户需要监听的参数,可能是函数也可能是一个reactive对象
  • 用户传入的回调函数

当需要监听的数据改变的时候,触发回调函数。那么实际上就相当于给监听的数据的属性绑定一个effect,做一个依赖收集,这样当数据改变,就触发用户的回调函数。
那么第一步对传入的数据做判断,如果是reacvtive就遍历数据对象,然后每个属性做依赖收集
如果是函数的话,就不用遍历。

function traversal(value, set = new Set()) {
  // 如果对象中有循环引用的问题 官方用Set
  if (isObject(value)) return value

  if (set.has(value)) return value
  set.add(value)

  for (let key in value) {
    traversal(value[key], set)
  }

  return value
}

export function watch(source,cb){
  let getter
  if (isReactive(source)) {
    // 对用户的传入的数据进行递归循环,只要循环就会访问对象的每一个属性,访问属性的时候就会收集effect。
    getter = () => traversal(source)
  } else if (isFunction(source)) {
    getter = source
  } else {
    return
  }
  // 同时记录下新值和旧值
   let oldValue
   let cleanup
  const job = () => {
    const newValue = effect.run()
    cb(newValue, oldValue, onCleanup)
    oldValue = newValue
  }
  // 在effect中属性就会被依赖收集
  const effect = new ReactiveEffect(getter, job) // 监控自己构造的函数,变化后重新执行job

  oldValue = effect.run()
}

这就是一个简单的watch函数。在wathc官方用法中,还遇到这样一种情况。例如input上面你输入文字然后进行接口调用搜索关键字,你输入了2个子。那么当第一个接口的延迟是2秒中之后返回数据,第二个文字接口在500毫秒返回数据。由于接口的调用是并行的。那么最终会才用第一个接口的数据,第二个比第一个快,导致被覆盖了,这是不正确的。
正常的做法有用防抖来做,那么vue官方提供了一个onCleanup回调函数。
当数据改变引起变化的时候,会调触发上一个watch回调的oncleanup。这样就能通过一些操作来渲染正确的数据。

const { reactive, watch } = VueReactivity
const state = reactive({ name: 'jw', address: { num: 1 } })
let getMoreData = (time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(state.address.num)
    }, time)
  })
}

watch(
  () => state.address.num,
  async (newValue, oldValue, onCleanup) => {
    let clean = false
    onCleanup(() => {
      clean = true
    })
    let i = Math.random() * 10000
    console.log(i)
    let text = await getMoreData(i)
    if (!clean) {
      document.getElementById('app').innerHTML = text
    }
  }
)
state.address.num = 456
state.address.num = 123
state.address.num = 678
state.address.num = 999

上面的分析,就是在触发回调函数的时候触发上一个watch中的onCleanup函数。
对watch增加一些功能,记录一下用户的onCleanup函数内容。

import { ReactiveEffect } from './effect'
import { isReactive } from './reactive'
import { isFunction, isObject } from '@vue/shared'

function traversal(value, set = new Set()) {
  // 如果对象中有循环引用的问题 官方用Set
  if (isObject(value)) return value

  if (set.has(value)) return value
  set.add(value)

  for (let key in value) {
    traversal(value[key], set)
  }

  return value
}

// source是用户传入的对象, cb 就是对应用户的回调
export function watch(source, cb) {
  let getter
  if (isReactive(source)) {
    // 对用户的传入的数据进行递归循环,只要循环就会访问对象的每一个属性,访问属性的时候就会收集effect。
    getter = () => traversal(source)
  } else if (isFunction(source)) {
    getter = source
  } else {
    return
  }
  let oldValue
  let cleanup
  const onCleanup = (fn) => {
    cleanup = fn // 保存用户的函数
  }

  const job = () => {
    if (cleanup) cleanup() // 下一次watch开始触发上一次watch的清理
    const newValue = effect.run()
    cb(newValue, oldValue, onCleanup)
    oldValue = newValue
  }
  // 在effect中属性就会被依赖收集
  const effect = new ReactiveEffect(getter, job) // 监控自己构造的函数,变化后重新执行job

  oldValue = effect.run()
}

针对props中数组的监听

在实际项目中常常需要编写组件,其中组件需要通过props接受参数。然后会遇到监听这些参数的改变做一些操作。那么就会遇到数组这种结构。

let props = defineProps({
    list:{
        type:Array,
        default:()=>[]
    }
})

// 写法一
watch(()=>props.list,(newVal,oldVal)=>{
    console.log(newVal)
})
// 写法二
watch(props.list,(newVal,oldVal)=>{
    console.log(newVal)
})
// 写法三
watch(()=>[...props.list],(newVal,oldVal)=>{
    console.log(newVal)
})

如果传入的数组使用reactive包裹的会出现写法一和写法三无法触发。
但是如果是ref包裹的,那么这段代码中,无法更改的是写法二,因为在 watch API 中,它只接受一个响应式对象作为第一个参数,而不能是一个 getter 函数。因此,写法一和写法三都是可以正常工作的,它们都会在 props.list 变化时触发 watch 回调函数并打印新的值。但是,写法三使用了一个新的数组,而不是原始的 props.list 数组,这意味着在数组中添加、删除或替换元素时,watch 回调函数将会触发。相比之下,写法一和写法二将只在 props.list 的引用更改时触发。

git:[@github/MicroMatrixOrg/vue3-plan/tree/watch]
0

评论区