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

  • 【源码系列#04】Vue3侦听器原理(Watch)已关闭评论
  • 67 次浏览
  • A+
所属分类:Web前端
摘要

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


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