【源码系列#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 ...
随机推荐
- day1项目配置
项目初始化 本项目使用vite进行构建,vite参考官网 pnpm包管理:performant npm,意味"高性能的npm".pnpm由npm/yarn衍生而来,解决了npmly ...
- Java流程控制10道题_上机
Java流程控制10道题 计算出1-100之间所有不能被3整除的整数的和大于(或等于)2000的数字. package day01; public class Lab01 { public stati ...
- 基于ZXing.NET实现的二维码生成和识别客户端
一.前言 ZXing.Net的一个可移植软件包,是一个开源的.多格式的1D/2D条形码图像处理库,最初是用Java实现的.已经过大量优化和改进,它已经被手动移植.它与.Net 2.0..Net 3.5 ...
- 深入解析Go非类型安全指针:技术全解与最佳实践
本文全面深入地探讨了Go非类型安全指针,特别是在Go语言环境下的应用.从基本概念.使用场景,到潜在风险和挑战,文章提供了一系列具体的代码示例和最佳实践.目的是帮助读者在保证代码安全和效率的同时,更加精 ...
- 外层div随内层div高度自适应
首先说一下textarea的高度随文字的内容自适应,用div模拟textarea.直接看代码.其中 contenteditable="true"表示div可以编辑..主要是设置 o ...
- TOBO
然而并不会做. 最后就照着题解码了一遍/kk 真的好长啊.看时间就知道写了多久... upd in 2022: 我现在已经找不到原题面是什么了( 不过感觉把这样一篇不算题解的东西放进"题解& ...
- 把工单历史表中的数据,按照工单进行分类,每一批工单的数据放在同一个 key 中
/** * 把工单历史表中的数据,按照工单进行分类,每一批工单的数据放在同一个key中 * 将对象列表按名称分类成 Map(分类名必与对象的某 get 方法的后的名一致<如:get(name) ...
- 电路中的N.M.缩写含义
国外的一些电路中会发现在一些器件旁会有 N.M. 的标注. N.M. = Not Mount
- 微前端、single-spa初探
微前端 微前端,前端这次词就不用多做解释了,这个概念的重点在于这个"微"字, 从字面意义上看,微是小的意思,小是相对于大的一个用于比较的形容词,所以通常是在项目庞大的情况下,才会考 ...
- 一种全新的日志异常检测评估框架:LightAD
本文分享自华为云社区<[AIOps]一种全新的日志异常检测评估框架:LightAD,相关成果已被软工顶会ICSE 2024录用>,作者: DevAI. 深度学习(DL)虽然在日志异常检测中 ...