当前位置: 首页 > news >正文

reactive和effect,依赖收集触发依赖

通过上一篇文章已经初始化项目,集成了tsjest。本篇实现Vue3中响应式模块里的reactive方法。

前置知识要求

如果你熟练掌握Map, Set, Proxy, Reflect,可直接跳过这部分。

Map

Map是一种用于存储键值对的集合,并且能够记住键的原始插入顺序。 其中键和值可以是任意类型的数据。

初始化,添加,获取

let myMap = new Map()myMap.set('name', 'wendZzoo')
myMap.set('age', 18)myMap.get('name')
myMap.get('age')

Map 中的一个键只能出现一次,它在 Map 的集合中是独一无二的,重复设置的会被覆盖

myMap.set('name', 'jack')

Map 的键和值可以是任意类型的数据

myMap.set({name: 'wendZzoo'}, [{age: 18}])

删除

let myMap = new Map()
myMap.set('name', 'Tom')
myMap.delete('name')

key数据类型是对象时,需要使用对应的引用来删除键值对

let myMap = new Map()
let key = [{name: 'Tom'}]
myMap.set(key, 'Hello')
myMap.delete(key)// 如果使用不同的引用来尝试删除键值对
// 它将无法正常工作
// 因为Map无法识别这两个引用是相同的键
myMap.set([{name: 'Tom'}], 'Hello')
myMap.delete([{name: 'Tom'}])

Set

Set是一种集合数据结构,它允许存储唯一的值,无重复项。Set对象可以存储任何类型的值,包括基本类型和对象引用。

let mySet = new Set()mySet.add('wendZzoo')
mySet.add(18)
mySet.add({province: 'jiangsu', city: 'suzhou'})

可迭代

for (let key of mySet) {console.log(key)
}

Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

Vue 响应式的前提就是需要数据劫持,在 JS 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制,而在 Vue 3 中则使用了 Proxy 来创建响应式对象。

创建 Proxy 对象时,需要提供两个参数:目标对象 target(被代理的对象)和一个处理程序对象 handler(用于定义拦截行为的方法)。

其中 handler 常用的有 get,set 方法。

handler.get() 方法用于拦截对象的读取属性操作,完整使用可以参考:MDN

它接收三个参数:

  1. target:目标对象
  2. property:被获取的属性名
  3. receiver:Proxy 或者继承 Proxy 的对象
const obj = {name: 'wendZzoo', age: 18}
let myProxy = new Proxy(obj, {get: (target, property, receiver) => {console.log('收集依赖')return target[property]}
})// 执行 myProxy.name
// 执行 myProxy.age

handler.set() 方法是设置属性值操作的捕获器,完整使用参考:MDN

它接收四个参数

  1. target:目标对象
  2. property:将被设置的属性名或 Symbol、
  3. value:新属性值
  4. receiver:最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)
const obj = {name: 'wendZzoo', age: 18}
let myProxy = new Proxy(obj, {get: (target, property, receiver) => {console.log('收集依赖')return target[property]},set: (target, property, value, receiver) => {console.log('触发依赖')target[property] = valuereturn true}
})
myProxy.name = 'Jack'
myProxy.age = 20

Proxy 提供了一种机制,通过拦截和修改目标对象的操作来实现自定义行为,在 get 和 set 方法打印日志的地方,也就是 Vue3 实现依赖收集和触发依赖的地方。

Reflect

Reflect 是一个内置的对象,它提供拦截 JS 操作的方法。这让它可以完美的和 Proxy 配合,Proxy 提供了对对象拦截的时机位置,Reflect 提供拦截方法。

Reflect 不是一个构造函数,因此不能 new 进行调用,更像 Math 对象,作为一个函数来调用,它所有的属性和方法都是静态的。

常用的方法有 get,set。

Reflect.get方法允许你从一个对象中取属性值,完整使用参考:MDN

它接收三个参数:

  1. target:需要取值的目标对象
  2. propertyKey:需要获取的值的键值
  3. receiver:如果 target 对象中指定了getter,receiver 则为 getter 调用时的this值
let obj = {name: 'wendZzoo', age: 18}
Reflect.get(obj, 'name')
Reflect.get(obj, 'age')

Reflect.set 方法允许在对象上设置属性,完整使用参考:MDN

它接收三个参数:

  1. target:设置属性的目标对象
  2. propertyKey:设置的属性的名称
  3. value:设置的值
  4. receiver:如果遇到 setter,receiver则为setter调用时的this值
let obj = {}
Reflect.set(obj, 'name', 'wendZzoo')let arr = ['name', 'address']
Reflect.set(arr, 1, 'age')
Reflect.set(arr, 'length', 1)

更改目录

src下新建文件夹reactivity,新建effect.tsreactive.ts

tests文件夹下删除上一篇文章中用于验证jest安装的index.spec.ts,新建effect.spec.tsreactive.spec.ts

reactive

先写单测,明确需要的成果,再根据这个需求来实现函数。Vue3的reactive方法返回一个对象的响应式代理,那代理的对象和源对象是不同的,但是又能和源对象一样的嵌套结构。

那单测可以这样写:reactive.spec.ts

import { reactive } from "../reactivity/reactive";describe("reactive", () => {it("happy path", () => {let original = { foo: 1 };let data = reactive(original);expect(data).not.toBe(original);expect(data.foo).toBe(1);});
});

根据这两个断言,来实现现阶段的reactive方法。Vue3中是使用Proxy实现。

reactive.ts

export function reactive(raw) {return new Proxy(raw, {get: (target, key) => {let res = Reflect.get(target, key);// TODO 依赖收集return res;},set: (target, key, value) => {let res = Reflect.set(target, key, value);// TODO 触发依赖return res;},});
}

运行reactive单测,来验证该方法实现是否正确,执行yarn test reactive

effect

在官网上是没有单独提到这个 API 的,可以在进阶主题的深入响应式系统一篇中找到它的身影。

effect直接翻译为作用,意思是使其发生作用,这个使其的其就是我们传入的函数,所以effect的作用就是让我们传入的函数发生作用,也就是执行这个函数。

使用示例

import { reactive, effect } from "vue";let user = reactive({age: 10,
});let nextAge;function setAge() {effect(() => {nextAge = user.age + 1;});console.log(nextAge);
}function updateAge() {user.age++;console.log(nextAge);
}

在没有使用effect作用于nextAge时,直接触发updateAge方法,输出的nextAge就是undefined

调用setAgeeffect中函数执行给nextAge赋值,响应式数据userage变化,nextAge也在继续执行effect中函数。

单测

effect的单测可以写成这样:

import { effect } from "../reactivity/effect";
import { reactive } from "../reactivity/reactive";describe("effect", () => {it("happy path", () => {let user = reactive({age: 10,});let nextAge;effect(() => {nextAge = user.age + 1;});expect(nextAge).toBe(11);});
});

effect方法就是接收一个方法,并执行它。

effect.ts

class ReactiveEffect {private _fn: any;constructor(fn) {this._fn = fn;}run() {this._fn();}
}export function effect(fn) {let _effect = new ReactiveEffect(fn);_effect.run();
}

通过抽离成一个Class类,去执行传入的 fn 参数。

再来执行所有的单测,验证是否成功,执行yarn test

依赖收集

修改effect单测,增加一个断言,来判断当age变化时,nextAge是否也更新了?

import { effect } from "../reactivity/effect";
import { reactive } from "../reactivity/reactive";describe("effect", () => {it("happy path", () => {let user = reactive({age: 10,});let nextAge;effect(() => {nextAge = user.age + 1;});expect(nextAge).toBe(11);// +++updateruser.age++;expect(nextAge).toBe(12);});
});

执行单测发现无法通过,是因为Proxy代理时候并没有实现依赖收集和触发依赖,也就是reactive.ts中还有两个 TODO。

但是,首先得清楚什么叫依赖

引用官方的例子:

let A0 = 1
let A1 = 2
let A2 = A0 + A1console.log(A2) // 3A0 = 2
console.log(A2) // 仍然是 3

当我们更改 A0 后,A2 不会自动更新。

那么我们如何在 JavaScript 中做到这一点呢?首先,为了能重新运行计算的代码来更新 A2,我们需要将其包装为一个函数:

let A2function update() {A2 = A0 + A1
}

然后,我们需要定义几个术语:

  • 这个 update() 函数会产生一个副作用,或者就简称为作用 (effect),因为它会更改程序里的状态。
  • A0 和 A1 被视为这个作用的依赖 (dependency),因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者 (subscriber)。

因此我们可以大胆通俗的讲,依赖就是指的是观察者(通常是视图或副作用函数)对数据的依赖关系。当观察者需要访问特定数据时,它就成为该数据的依赖。

依赖收集呢?

依赖收集是用于追踪和管理数据依赖关系。常用于实现响应式系统,其中数据的变化会自动触发相关的更新操作。

当数据发生改变时,相关的视图或操作也能够自动更新,以保持数据和界面的同步。依赖收集可以帮助我们建立起数据和视图之间的关联,确保数据的变化能够自动反映在视图上。

从代码层面讲,读取对象的时候也就是get操作时,进行依赖收集,将目标对象target,对象中key,Dep实例做关联映射。

effect.ts中定义依赖收集的方法track

class ReactiveEffect {private _fn: any;constructor(fn) {this._fn = fn;}run() {reactiveEffect = this;this._fn();}
}let targetMap = new Map();
export function track(target, key) {// target -> key -> deplet depMap = targetMap.get(target);if (!depMap) { // initdepMap = new Map();targetMap.set(target, depMap);}let dep = depMap.get(key);if (!dep) { // initdep = new Set();depMap.set(key, dep);}dep.add(reactiveEffect);
}let reactiveEffect;
export function effect(fn) {let _effect = new ReactiveEffect(fn);_effect.run();
}

触发依赖

在设置对象属性时,也就是进行set操作时,触发依赖。将每个属性上挂载的depSet结构中的所有作用函数执行。

export function trigger(target, key) {let depMap = targetMap.get(target);let dep = depMap.get(key);for (const effect of dep) {effect.run();}
}

至此,再次执行所有单测,yarn test

总结

  1. 先通过单测入手,明确需要实现的函数方法的功能
  2. 分布实现功能点,即拆分功能点,先初步实现了reactive方法简单版,只要求原数据和代理之后的数据不同,但是数据结构又要一样,像深拷贝一样。
  3. 通过Class类,实现effect方法可以自执行其传入的函数参数
  4. 依赖收集,通过两个Map结构和一个Set结构来映射数据关系,将所有的fn存放到dep中。通过一个全局变量reactiveEffect来获取到effct实例,为后续触发依赖时,直接拿dep中每一项去执行。
  5. 触发依赖,通过映射关系获取到dep,因为depSet结构,可迭代,循环每项执行。

相关文章:

reactive和effect,依赖收集触发依赖

通过上一篇文章已经初始化项目,集成了ts和jest。本篇实现Vue3中响应式模块里的reactive方法。 前置知识要求 如果你熟练掌握Map, Set, Proxy, Reflect,可直接跳过这部分。 Map Map是一种用于存储键值对的集合,并且能够记住键的原始插入顺…...

【C#学习】backgroundWorker控件

BackgroundWorker 控件的几个实例(C# backgroundworker使用方法): 在 WinForms 中,有时要执行耗时的操作,在该操作未完成之前操作用户界面,会导致用户界面停止响应。 解决的方法就是新开一个线程&#xff…...

Istio学习笔记-部署模型

参考:Istioldie 1.18 / 部署模型 当您将 Istio 用于生产环境部署时,需要确定一系列的问题。 网格将被限制在单个集群中还是分布在多个集群中? 是将所有服务都放置在单个完全连接的网络中,还是需要网关来跨多个网络连接服务&#…...

磁盘调度算法

磁盘调度算法是计算机操作系统中用于管理磁盘上的数据访问的重要组成部分。这些算法有助于优化数据的读写操作,以减少磁盘访问时间,提高系统性能。以下是一些常见的磁盘调度算法: 先来先服务(FCFS,First-Come-First-Se…...

力扣题库2. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外,这两个数都不会以 0 开…...

【Linux】第十六站:进程地址空间

文章目录 一、程序地址空间1.内存的分布2.static修饰后为什么不会被释放3.一个奇怪的现象 二、进程地址空间1.前面现象的原因2.地址空间究竟是什么?3.为什么要有进程地址空间4.页表5.什么叫进程?6.进程具有独立性。为什么?怎么做到呢&#xf…...

基于Springboot的影城管理系统(有报告)。Javaee项目,springboot项目。

演示视频: 基于Springboot的影城管理系统(有报告)。Javaee项目,springboot项目。 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 项目介绍…...

如何在面试中胜出?接口自动化面试题安排上

📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢交流讨论:欢迎加入我们一起学习!📢资源分享:耗时200小时精选的「软件测试」资…...

联邦学习研究综述笔记

联邦学习 联邦学习的定义:联邦学习是一种分布式机器学习架构,包含多个客户端(参与者)和一个聚合服务器。客服端(参与方):在本地使用自己的私有数据训练模型,训练完成之后将模型的参…...

RedisTemplate乱码问题

其实这是在解决一个项目问题是发现的,因为原开发者的大意,造成了系统出现严重的逻辑问题。 因为系统系统采用分模块开发,某模块使用Spring提供的RedisTemplate进行值的读写,另一位使用了框架基于Jedis的一套公用方法进行值的读写…...

Java用户和内核交互图

...

2023.11.14使用bootstrap制作一个简洁的前端注册登录页

2023.11.14使用bootstrap制作一个简洁的前端注册登录页 比较简洁的登录页,主要是为自己开发的一些平台页面做测试用,前端具备功能如下: (1)输入用户名、密码,需补充后端验证代码。 (2&#xff…...

Avatar虚拟形象解决方案,趣味化的视频拍摄与直播新体验

企业们正在寻找新的方式来吸引和保持观众的注意力,一种新兴的解决方案就是使用Avatar虚拟形象技术,这种技术可以让用户在视频拍摄或直播场景中,以自定义的数字人形象出现,同时保持所有的表情和脸部驱动。美摄科技正是这个领域的领军者&#x…...

MongoDB备份与恢复以及导入导出

MongoDB备份与恢复 1、mongodump数据备份 在Mongodb中我们使用mongodump命令来备份MongoDB数据。该命令可以导出所有数据 (数据和数据结构) 或指定数据(集合、部分集合内容)到指定目录中。 语法: mongodump -h dbhost -d dbname -o dbdirec…...

如何挑选猫主食罐头?宠物店自用的5款猫主食罐头推荐!

临近双十二大促,是时候给家里的猫主子屯猫主食罐头了。许多铲屎官看大促的各种品牌宣传,看到眼花缭乱,不知道选哪些猫主食罐头好,胡乱选又怕踩坑。 猫罐头侠闪亮登场!如何挑选猫主食罐头?作为经营宠物店7年…...

立哥先进研发-API安全方案

项目背景:随着技术进步,很多优秀技术也被用在黑灰产之中,例如爬虫系统在票务系统中的滥用,尤其机票系统。机票爬虫们威力之大,让人叹为观止:多数订票网站,真实用户产生的不到10%,其浏…...

小函数:Lambda表达式(Java篇)

Lambda表达式的使用场景:用以简化接口实现。 关于接口实现,可以有很多种方式来实现。例如:设计接口的实现类、使用匿名内部类。 但是lambda表达式,比这两种方式都简单! lambda表达式毕竟只是⼀个匿名方法。当实现的接…...

RSS订阅快速连接Notion

数环通让您可以通过不到几分钟的时间即可实现RSS订阅与Notion的对接与集成,从而高效实现工作流程自动化,降本增效! 1.产品介绍 RSS订阅是数环通的内置应用,很多用户通过RSS订阅来收集自己在各大平台上看的内容,当RSS…...

VMware ubuntu 新虚拟机的创建

根据自己指定的路径安装好vm后。 创建新的虚拟机。 记录一下,下次用到别再忘记了。 如需转载,注明出处! 点赞收藏关注我 以资鼓励 打开vm 软件,点击创建新的虚拟机 选择典型,点击下一步 选择你的ubuntu镜像iso文件 …...

第一篇 《随机点名答题系统》简介及设计流程图(类抽奖系统、在线答题系统、线上答题系统、在线点名系统、线上点名系统、在线考试系统、线上考试系统)

专栏目录 第一篇 《随机点名答题系统》简介及设计流程图(类抽奖系统、在线答题系统、线上答题系统、在线点名系统、线上点名系统、在线考试系统、线上考试系统)-CSDN博客 第二篇 《随机点名答题系统》——题库管理详解(类抽奖系统、在线答题…...

Chapter03-Authentication vulnerabilities

文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

React第五十七节 Router中RouterProvider使用详解及注意事项

前言 在 React Router v6.4 中&#xff0c;RouterProvider 是一个核心组件&#xff0c;用于提供基于数据路由&#xff08;data routers&#xff09;的新型路由方案。 它替代了传统的 <BrowserRouter>&#xff0c;支持更强大的数据加载和操作功能&#xff08;如 loader 和…...

反射获取方法和属性

Java反射获取方法 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是一种强大的机制&#xff0c;允许程序在运行时访问和操作类的内部属性和方法。通过反射&#xff0c;可以动态地创建对象、调用方法、改变属性值&#xff0c;这在很多Java框架中如Spring和Hiberna…...

【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)

1.获取 authorizationCode&#xff1a; 2.利用 authorizationCode 获取 accessToken&#xff1a;文档中心 3.获取手机&#xff1a;文档中心 4.获取昵称头像&#xff1a;文档中心 首先创建 request 若要获取手机号&#xff0c;scope必填 phone&#xff0c;permissions 必填 …...

今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存

文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!

简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求&#xff0c;并检查收到的响应。它以以下模式之一…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf

FTP 客服管理系统 实现kefu123登录&#xff0c;不允许匿名访问&#xff0c;kefu只能访问/data/kefu目录&#xff0c;不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join

纯 Java 项目&#xff08;非 SpringBoot&#xff09;集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...

适应性Java用于现代 API:REST、GraphQL 和事件驱动

在快速发展的软件开发领域&#xff0c;REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名&#xff0c;不断适应这些现代范式的需求。随着不断发展的生态系统&#xff0c;Java 在现代 API 方…...

在 Spring Boot 中使用 JSP

jsp&#xff1f; 好多年没用了。重新整一下 还费了点时间&#xff0c;记录一下。 项目结构&#xff1a; pom: <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://ww…...