【源码系列#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 ...
随机推荐
- 逻辑漏洞挖掘之XSS漏洞原理分析及实战演练
一.前言 2月份的1.2亿条用户地址信息泄露再次给各大公司敲响了警钟,数据安全的重要性愈加凸显,这也更加坚定了我们推行安全测试常态化的决心.随着测试组安全测试常态化的推进,有更多的同事对逻辑漏洞产生了 ...
- LINUX线程之一次性初始化(PTHREAD_ONCE)
1.一次性初始化 在 Linux函数列表 中描述了Linux线程中的常用函数,这里详细讲解 pthread_once 函数的功能和使用. (1)为何有"一次性初始化概念"出现? 其 ...
- c语言代码练习9
\\判断1000-2000之间的闰年有哪些,有几个 #define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h> int main(){ //判断 ...
- Vue2系列(lqz)——Vue介绍
文章目录 Vue入门 零:前端目前形势 前端的发展史 一:Vue介绍 和 基本使用 1.Vue介绍 渐进式框架 网站 2.Vue特点 易用 灵活 高效 3.M-V-VM思想 ① MVVM介绍 ② MV ...
- 计算机三级网络技术备考复习资料zhuan
计算机三级网络技术备考复习资料 第一章 计算机基础 分析:考试形式:选择题和填空题,6个的选择题和2个填空题共10分,都是基本概念 1.计算机的四特点:有信息处理的特性,有广泛适应的特性,有 ...
- CF1526C2
与简单版的思路完全一致,只需要改一下范围. 可以去看我简单版本的博客. 题目简化和分析: 给您一个数组,在其中选择若干个数使得: 任意前缀和 \(\ge 0\) 数量尽可能的大 我们可以使用贪心策略, ...
- 2023-10-14:用go语言,给定 pushed 和 popped 两个序列,每个序列中的 值都不重复, 只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时, 返回
2023-10-14:用go语言,给定 pushed 和 popped 两个序列,每个序列中的 值都不重复, 只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时, 返回 ...
- 14. 从零开始编写一个类nginx工具, HTTP文件服务器的实现过程及参数
wmproxy wmproxy将用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,后续将实现websocket代理, 内外网穿透等, 会将实现过程分享出来, 感 ...
- 【虹科干货】Redis 开发者需要了解的缓存驱逐策略
在你搭建并配置了一个Redis数据库之后,Redis成功地提升了应用程序性能.然而这里有一个潜在问题,随着缓存数据的快速增加和内存占用率的逐渐上升,你很快会发现Redis缓存容量即将达到硬件存储容量上 ...
- 导出所有容器id号
#!/bin/bash a=`docker ps|awk 'NR>1{print $1}'` FORMAT="%-12s\t,\t%-12s\t,\t%-12s\n" pri ...