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

【源码系列#04】Vue3侦听器原理(Watch)的更多相关文章

  1. 大白话Vue源码系列(04):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  2. 【Vue2.x源码系列06】计算属性computed原理

    上一章 Vue2异步更新和nextTick原理,我们介绍了 JavaScript 执行机制是什么?nextTick源码是如何实现的?以及Vue是如何异步更新渲染的? 本章目标 计算属性是如何实现的? ...

  3. JVM源码系列:ThreadMXBean 打出堆栈信息原理分析

    我们通常会使用工具jstack 去跟踪线程信息,其如何实现使用attach 的方式还是ptrace 的方式,这些可以去参考本人的博客的其他文章. 但这些方式都是外部使用的方式,如何直接使用java代码 ...

  4. Vue.js 源码分析(七) 基础篇 侦听器 watch属性详解

    先来看看官网的介绍: 官网介绍的很好理解了,也就是监听一个数据的变化,当该数据变化时执行我们的watch方法,watch选项是一个对象,键为需要观察的数据名,值为一个表达式(函数),还可以是一个对象, ...

  5. Typescript | Vue3源码系列

    TypeScript 是开源的,TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript.编译出来的 JavaScript 可以运行在任何浏览器上.TypeS ...

  6. Vue.js之Vue计算属性、侦听器、样式绑定

    前言 上一篇介绍了Vue的基本概念,这一篇介绍一下Vue的基本使用. 一.搭建一个Vue程序 1.1 搭建Vue环境 搭建Vue的开发环境总共有三种方法: 引入CDN <script src=& ...

  7. 【Vue2.x源码系列07】监听器watch原理

    上一章 Vue2计算属性原理,我们介绍了计算属性是如何实现的?计算属性缓存原理?以及洋葱模型是如何应用的? 本章目标 监听器是如何实现的? 监听器选项 - immediate.deep 内部实现 初始 ...

  8. 事件机制-Spring 源码系列(4)

    事件机制-Spring 源码系列(4) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Ioc容器BeanPostProcess ...

  9. AOP执行增强-Spring 源码系列(5)

    AOP增强实现-Spring 源码系列(5) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Ioc容器BeanPostProc ...

  10. 大白话Vue源码系列(05):运行时鸟瞰图

    阅读目录 Vue 实例的生命周期 实例创建 响应的数据绑定 挂载到 DOM 节点 结论 研究 runtime 一边 Vue 一边源码 初看 Vue 是 Vue 源码是源码 再看 Vue 不是 Vue ...

随机推荐

  1. textarea自动适应高度

    textarea自动适应高度,兼容IE/Firefox.chrome 代码:<textarea name="textarea" id="textarea" ...

  2. T-SQL——关于数据合并(Merge)

    目录 0. 背景说明及测试数据 1. 直接清空,重新插入 2. 单条记录执行插入.更新操作 3. Merge函数 3.1 准备测试数据 3.2 测试Merge 3.3 关于Merge 4.参考 sha ...

  3. Windows上Dart安装

    过程 *1 去github上下载一个release包或者直接将flutter通过git clone下来 *2 将下载下来的flutter/bin添加到path中 *3 此时运行flutter或者flu ...

  4. fmt 库简介和示例【GO 基础】

    〇.关于 fmt fmt 标准库是 Go 语言标准库的一部分,提供了格式化字符串.输入输出等基本功能.通过 fmt 库,我们可以进行简单的格式化输出.读取用户输入.错误输出等操作. fmt 库实现了类 ...

  5. FFmpeg FFmpeg

     FFmpeg About News Download Documentation Community Code of Conduct Mailing Lists IRC Forums Bug Rep ...

  6. Android项目Library导入的问题整理

    Android项目Library导入的问题整理 本来帮助朋友找寻一下android的一些特效的demo,结果找到了一个,朋友试验可以,自己却是在导入项目需要的library的时候总是出问题,真的很是丢 ...

  7. 圆角android

    资源地址 <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid an ...

  8. vcpkg manifest 的使用

    最近项目上要使用 CMakeLists 管理,由于 Windows 版本有依赖到 vcpkg 提供的库,所以需要使用 vcpkg manifest 来统一设置库的版本,方便后续维护 推荐一个文章,介绍 ...

  9. Vue之键盘事件

    1.使用keydown触发事件 <!DOCTYPE html> <html lang="en"> <head> <meta charset ...

  10. FFT & NTT 及其简单优化

    FFT FFT 是一种高效实现 DFT 和 IDFT 的方式,可以在 \(O(n \log n)\) 的时间内求多项式的乘法. 多项式的点值表示 不同于用每项的系数来表示一个多项式,我们知道对于给定的 ...