【源码系列#04】Vue3侦听器原理(Watch)
专栏分享: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
  },
)
数据源
- 数据源为 ref 的情况,和 immediate 回调执行时机
const x = ref(1)
watch(
  x,
  (newValue, oldValue) => {
    console.log('x', newValue, oldValue)
  }, {
    immediate: true
  },
)
setTimeout(() => {
  x.value = 2
}, 100)
- 兼容数据源为 响应式对象 和 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)
【源码系列#04】Vue3侦听器原理(Watch)的更多相关文章
- 大白话Vue源码系列(04):生成render函数
		阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ... 
- 【Vue2.x源码系列06】计算属性computed原理
		上一章 Vue2异步更新和nextTick原理,我们介绍了 JavaScript 执行机制是什么?nextTick源码是如何实现的?以及Vue是如何异步更新渲染的? 本章目标 计算属性是如何实现的? ... 
- JVM源码系列:ThreadMXBean 打出堆栈信息原理分析
		我们通常会使用工具jstack 去跟踪线程信息,其如何实现使用attach 的方式还是ptrace 的方式,这些可以去参考本人的博客的其他文章. 但这些方式都是外部使用的方式,如何直接使用java代码 ... 
- Vue.js 源码分析(七) 基础篇 侦听器 watch属性详解
		先来看看官网的介绍: 官网介绍的很好理解了,也就是监听一个数据的变化,当该数据变化时执行我们的watch方法,watch选项是一个对象,键为需要观察的数据名,值为一个表达式(函数),还可以是一个对象, ... 
- Typescript | Vue3源码系列
		TypeScript 是开源的,TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript.编译出来的 JavaScript 可以运行在任何浏览器上.TypeS ... 
- Vue.js之Vue计算属性、侦听器、样式绑定
		前言 上一篇介绍了Vue的基本概念,这一篇介绍一下Vue的基本使用. 一.搭建一个Vue程序 1.1 搭建Vue环境 搭建Vue的开发环境总共有三种方法: 引入CDN <script src=& ... 
- 【Vue2.x源码系列07】监听器watch原理
		上一章 Vue2计算属性原理,我们介绍了计算属性是如何实现的?计算属性缓存原理?以及洋葱模型是如何应用的? 本章目标 监听器是如何实现的? 监听器选项 - immediate.deep 内部实现 初始 ... 
- 事件机制-Spring 源码系列(4)
		事件机制-Spring 源码系列(4) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Ioc容器BeanPostProcess ... 
- AOP执行增强-Spring 源码系列(5)
		AOP增强实现-Spring 源码系列(5) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Ioc容器BeanPostProc ... 
- 大白话Vue源码系列(05):运行时鸟瞰图
		阅读目录 Vue 实例的生命周期 实例创建 响应的数据绑定 挂载到 DOM 节点 结论 研究 runtime 一边 Vue 一边源码 初看 Vue 是 Vue 源码是源码 再看 Vue 不是 Vue ... 
随机推荐
- Denpendcy Injection 8.0新功能——KeyedService
			Denpendcy Injection 8.0新功能--KeyedService 本文只介绍 .NET Denpendcy Injection 8.0新功能--KeyedService,假定读者已熟练 ... 
- SpringBoot WebSocket STOMP
			SpringBoot WebSocket STOMP 关键词:Springboot, WebSocket, STOMP, broadcast, sendToUser, MessageMapping, ... 
- 前端三件套系例之JQuery——JQuery动画效果、JQuery插件、
			文章目录 1 JQuery动画效果 1. 基本效果 2. 滑动效果 3 淡入淡出效果 4 自定义动画 5 动画控制 6 设置 7 事件 7-1 常用事件 7-2 事件绑定 7-3 移除事件 7-4 阻 ... 
- 文心一言 VS 讯飞星火 VS chatgpt (108)-- 算法导论10.1 6题
			六.用go语言,说明如何用两个栈实现一个队列,并分析相关队列操作的运行时间. 文心一言: 使用两个栈实现队列的基本思想是利用一个栈(stack1)来处理入队操作,另一个栈(stack2)来处理出队操作 ... 
- C、C++函数和类库详解(GCC版)(2014-4-23更新)
			C.C++函数和类库详解(GCC版)(未完成) 整理者:高压锅 QQ:280604597 Email:280604597@qq.com 大家有什么不明白的地方,或者想要详细了解的地方可以联系我,我会认 ... 
- Jenkins相关概念
			1,Jenkins相关工具概念: 要熟练掌握Jenkins持续集成的配置.使用和管理,需要了解相关的概念.例如代码开发.编译.打包.构建等名称,常见的代码相关概念包括:JDK.JAVA.MAKE.AN ... 
- LVS+keepalived结合
			LVS+Keepalived实现高可用负载均衡(web集群) LVS+Keepalived架构图: 测试环境: 名称 操作系统 IP地址 LVS-MASTER Centos7.x 192.168. ... 
- angular:响应式表单(Reactive Forms)和模板驱动表单(Template-Driven Forms)分别进行验证
			2022-01-18 响应式表单 响应式表单是围绕Observable的流构建的. 使用响应式表单时,FormControl类是最基本的构造类. 在使用响应式表单前,需要先导入 ReactiveFor ... 
- 阿里云上的rds 的隔离级别read committed 而不是repeatable-read设置原因
			阿里云上的rds 的隔离级别 是read committed ,而不是原生mysql的"可重复读(repeatable-read)",他们是基于什么原因这样设置的? show va ... 
- Kubernetes Gateway API 攻略:解锁集群流量服务新维度!
			Kubernetes Gateway API 刚刚 GA,旨在改进将集群服务暴露给外部的过程.这其中包括一套更标准.更强大的 API资源,用于管理已暴露的服务.在这篇文章中,我将介绍 Gateway ... 
