专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核💪推荐🙌
欢迎各位ITer关注点赞收藏🌸🌸🌸

语法

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数

const x = ref(0)const y = ref(0)// 单个 refwatch(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 = personwatch(  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 收集了 effectwatch(  () => 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 = 3000function getData(timer) {  return new Promise((resolve, reject) => {    setTimeout(() => {      resolve(timer)    }, timer)  })}// 1. 第一次调用watch的时候注入一个取消的回调// 2. 第二次调用watch的时候会执行上一次注入的回调// 3. 第三次调用watch会执行第二次注入的回调// 后面的watch触发会将上次watch中的 clear 置为truewatch(  () => 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 = 26setTimeout(() => {  person.age = 27}, 0)

人间不正经生活手册