《Vue进阶教程》(11)watch的实现详细教程

server/2024/12/25 14:40:17/

1 基本概念

1) 什么是watch

watch叫侦听器, 侦听某个响应式数据, 当数据改变时, 重新执行对应的回调

所以, watch可以建立数据->函数的对应关系

2) 基本使用

第一种: 接收引用了属性的副作用函数做为参数

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./vue.js"></script></head><body><script>const { reactive, watch } = Vueconst state = reactive({ name: 'xiaoming', address: { city: '武汉' } })// watch侦听副作用函数(引用代理对象的属性)watch(() => state.name,() => {console.log('只有当侦听的某个属性改变时, 才执行')})setTimeout(() => {// 只有当state.name属性改变时, 才执行state.name = 'xiaopang'// state.address.city = '北京' // 修改city不会触发更新}, 1000)</script></body>
</html>

第二种: 接收响应式对象做为参数

  1. watch侦听响应式对象时, 默认是深度侦听
  2. watch对应的回调默认不执行, 只有当属性改变时执行
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./vue.js"></script></head><body><script>const { reactive, watch } = Vueconst state = reactive({ name: 'xiaoming' })// watch接收响应式对象的情况watch(state, () => {console.log('该函数默认不执行, 只有状态更新时执行...')})setTimeout(() => {state.name = 'xiaopang'}, 1000)</script></body>
</html>
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./vue.js"></script></head><body><script>const { reactive, watch } = Vueconst state = reactive({ name: 'xiaoming', address: { city: '武汉' } })// watch接收响应式对象的情况watch(state, () => {console.log('该函数默认不执行, 只有状态更新时执行...')})setTimeout(() => {// 侦听对象时, 是深度侦听state.address.city = '北京'}, 1000)</script></body>
</html>

2 实现侦听函数

watch的实现实际上跟effect非常类似

effect接受两个参数

  1. 属性关联的副作用函数(类似watch的第一个参数)
  2. 更新时执行的调度函数(类似watch的第二个参数)

因此, 我们考虑基于effect初步实现watch的功能

如果第一个参数就是一个副作用函数, watch跟effect的参数一致

function watch(source, cb) {effect(source, {scheduler() {cb()},})
}

3 实现侦听对象

1) 基本实现

如果第一个参数不是副作用函数, 可以将其包装成一个副作用函数

function watch(source, cb) {let getterif (typeof source === 'function') {getter = source} else {getter = () => source}effect(getter, {scheduler() {cb()},})
}

由于并没有触发代理对象的取值操作, 因此不会收集依赖

考虑实现一个函数, 遍历访问source中的每一个属性

function traverse(value) {for (const k in value) {// 将代理对象的每个属性访问一次value[k]}
}
function watch(source, cb) {let getterif (typeof source == 'function') {getter = source} else {getter = () => traverse(source)}effect(getter, {scheduler() {cb()},})
}

2) 支持嵌套

如果代理对象中存在嵌套情况

  1. 在reactive中要递归为嵌套的对象创建代理
  2. 在traverse中要递归访问嵌套的属性

示例

get(target, key) {if (key == '__v_isReactive') return true // 新增// console.log(`自定义访问${key}`)// 收集依赖track(target, key)if (typeof target[key] == 'object') {// 递归处理对象类型return reactive(target[key])}return target[key]
},
function traverse(value) {for (const k in value) {if (typeof value[k] == 'object') {traverse(value[k])} else {// 将代理对象的每个属性访问一次value[k]}}
}

3) 解决循环引用问题

如果按上述写法, 会出现递归循环引用, 陷入死循环

什么是循环引用

如果一个对象的某个属性引用自身, 在递归时会死循环

问题示例

const state = reactive({ name: 'xiaoming', address: { city: '武汉' } })
state.test = state// watch接收响应式对象的情况
watch(state, () => {console.log('该函数默认不执行, 只有状态更新时执行...')
})setTimeout(() => {// 侦听对象时, 是深度侦听state.address.city = '北京'
}, 1000)

以上问题可以简化为

const obj = {foo: 'foo',
}
// obj的一个属性引用obj本身, 出现循环引用问题
obj.bar = objfunction traverse(value) {for (const k in value) {if (typeof value[k] == 'object') {traverse(value[k])} else {// 将代理对象的每个属性访问一次value[k]}}
}traverse(obj)

为了避免递归循环引用陷入死循环, 改造traverse方法

function traverse(value, seen = new Set()) {// 如果这个对象已经被遍历过了, 直接返回if (typeof value !== 'object' || seen.has(value)) return// 将遍历的对象记录下来, 再递归时判断seen.add(value)for (let key in value) {// 这里取值, 会触发proxy对象的getter操作traverse(value[key], seen)}
}

4 实现新旧值

我们知道, 在watch的回调中可以获取新值和旧值. 是如何实现的呢?

基本使用

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./vue.js"></script></head><body><script>const { reactive, watch } = Vueconst state = reactive({ name: 'xiaoming', address: { city: '武汉' } })watch(() => state.name,(newValue, oldValue) => {console.log(newValue, oldValue)})setTimeout(() => {state.name = 'xiaopang'}, 1000)</script></body>
</html>

具体实现

function watch(source, cb) {let getterif (typeof source == 'function') {getter = source} else {getter = () => traverse(source)}let oldValue, newValueconst _effect = effect(getter, {lazy: true, // 手动控制run的时机scheduler() {newValue = _effect.run()cb(newValue, oldValue)oldValue = newValue},})oldValue = _effect.run()
}

5 立即执行的回调

默认情况下, watch中的回调是不执行的. 但是可以通过传入参数让其立即执行

第一次立即执行回调时, 拿到的旧值是undefined

基本使用

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./vue.js"></script></head><body><script>const { reactive, watch } = Vueconst state = reactive({ name: 'xiaoming', address: { city: '武汉' } })watch(() => state.name,(newValue, oldValue) => {console.log('设置immediate选项会立即执行回调')console.log(newValue, oldValue)},{ immediate: true })setTimeout(() => {state.name = 'xiaopang'}, 1000)</script></body>
</html>

具体实现

立即执行的函数和更新时执行的函数本质上是没有区别的

因此, 我们可以将scheduler封装起来

function watch(source, cb, options = {}) {let getterif (typeof source == 'function') {getter = source} else {getter = () => traverse(source)}let oldValue, newValueconst job = () => {newValue = _effect.run()cb(newValue, oldValue)oldValue = newValue}const _effect = effect(getter, {lazy: true,scheduler: job,})if (options.immediate) {job()} else {oldValue = _effect.run()}
}

6 watchEffect

watchEffect也可以用于侦听属性的改变.

基本用法

  1. 接受副作用函数作为参数
  2. 自动收集依赖
  3. 不关心新旧值
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="./vue.js"></script></head><body><script>const { reactive, watchEffect } = Vueconst state = reactive({ name: 'xiaoming' })watchEffect(() => {console.log(state.name)})setTimeout(() => {state.name = 'xiaopang'}, 1000)</script></body>
</html>

具体实现

watchEffect跟effect是非常类似的.

由于只接受一个副作用函数作为参数. 注册和更新时都执行这个函数.

基本逻辑可以跟watch复用.

function doWatch(source, cb, options = {}) {let getterif (typeof source == 'function') {getter = source} else {getter = () => traverse(source)}let oldValue, newValueconst job = () => {if (cb) {newValue = _effect.run()cb(newValue, oldValue)oldValue = newValue} else {_effect.run()}}const _effect = effect(getter, {lazy: true,scheduler: job,})if (options.immediate) {job()} else {oldValue = _effect.run()}
}
function watch(source, cb, options = {}) {doWatch(source, cb, options)
}
function watchEffect(source, options = {}) {doWatch(source, null, options)
}


http://www.ppmy.cn/server/153058.html

相关文章

【算法】一维二维数组前缀和,以及计算二维矩阵中的子矩阵和

前缀和的概念 通过构建一个前缀和数组&#xff0c;我们可以在常数时间&#xff08;O(1)&#xff09;内使用前缀和数组计算任意子数组或子矩阵的和。 简单来说&#xff0c;就是把前面的项加在一起&#xff0c;使得新构建的前缀和数组中每一项都是原数组对应项之前的总和。 一…

HTTP 协议规定的协议头和请求头

一、协议头&#xff08;HTTP Headers&#xff09;概述 HTTP 协议头是 HTTP 请求和响应消息的一部分&#xff0c;它们包含了关于消息的各种元信息。这些信息对于客户端和服务器之间正确地传输和理解数据至关重要。 协议头可以分为请求头&#xff08;Request Headers&#xff0…

低代码软件搭建自学第二天——构建拖拽功能

文章目录 第 3 步&#xff1a;实现拖拽功能3.1 拖拽的基本概念3.2 创建基础拖拽界面代码示例&#xff1a;拖拽矩形运行结果&#xff1a; 3.3 添加多个拖拽元素代码示例&#xff1a;多个拖拽元素运行结果&#xff1a; 3.4 添加工具箱代码示例&#xff1a;工具箱 拖拽运行结果&a…

GESP CCF C++八级编程等级考试认证真题 2024年12月

202412 GESP CCF C八级编程等级考试认证真题 1 单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09; 第 1 题 小杨家响应国家“以旧换新”政策&#xff0c;将自家的汽油车置换为新能源汽车&#xff0c;正在准备自编车牌。自编车牌包括5 位数字或英文字母&#xff0c…

力扣题目解析--两数相除

题目 给你两个整数&#xff0c;被除数 dividend 和除数 divisor。将两数相除&#xff0c;要求 不使用 乘法、除法和取余运算。 整数除法应该向零截断&#xff0c;也就是截去&#xff08;truncate&#xff09;其小数部分。例如&#xff0c;8.345 将被截断为 8 &#xff0c;-2.…

微调大模型时,如何进行数据预处理? 将<input, output>转换为模型所需的<input_ids, labels, attention_mask>

原始训练数据集格式如下&#xff1a; <input, output> 形式&#xff1a;字符 模型训练所需数据格式如下&#xff1a; # tokenizer处理后 return {"input_ids": example,"labels": labels,"attention_mask": example_mask, } 将字符转…

鸿蒙项目云捐助第十七讲云捐助我的页面上半部分的实现

鸿蒙项目云捐助第十七讲云捐助我的页面上半部分的实现 在一般的应用app中都会有一个“我的”页面&#xff0c;在“我的”页面中可以完成某些设置&#xff0c;也可以完成某些附加功能&#xff0c;如“修改密码”等相关功能。这里的鸿蒙云捐助也有一个“我的”功能页面。这里对“…

第二节:让电机转起来【51单片机-L298N-步进电机教程】

摘要&#xff1a;本节介绍用简单的方式&#xff0c;让步进电机转起来。其目的之一是对电机转动有直观的感受&#xff0c;二是熟悉整个开发流程 本系列教程必要的51单片机基础包括IO口操作、中断、定时器三个部分&#xff0c;可先行学习 一、软件清单 需要用到的软件有keil5编译…