reactive和effect,依赖收集触发依赖
通过上一篇文章已经初始化项目,集成了ts和jest。本篇实现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
它接收三个参数:
- target:目标对象
- property:被获取的属性名
- 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
它接收四个参数
- target:目标对象
- property:将被设置的属性名或 Symbol、
- value:新属性值
- 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
它接收三个参数:
- target:需要取值的目标对象
- propertyKey:需要获取的值的键值
- receiver:如果 target 对象中指定了getter,receiver 则为 getter 调用时的this值
let obj = {name: 'wendZzoo', age: 18}
Reflect.get(obj, 'name')
Reflect.get(obj, 'age')

Reflect.set 方法允许在对象上设置属性,完整使用参考:MDN
它接收三个参数:
- target:设置属性的目标对象
- propertyKey:设置的属性的名称
- value:设置的值
- 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.ts和reactive.ts。
tests文件夹下删除上一篇文章中用于验证jest安装的index.spec.ts,新建effect.spec.ts和reactive.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;
调用setAge,effect中函数执行给nextAge赋值,响应式数据user中age变化,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操作时,触发依赖。将每个属性上挂载的dep的Set结构中的所有作用函数执行。
export function trigger(target, key) {let depMap = targetMap.get(target);let dep = depMap.get(key);for (const effect of dep) {effect.run();}
}
至此,再次执行所有单测,yarn test

总结
- 先通过单测入手,明确需要实现的函数方法的功能
- 分布实现功能点,即拆分功能点,先初步实现了
reactive方法简单版,只要求原数据和代理之后的数据不同,但是数据结构又要一样,像深拷贝一样。 - 通过
Class类,实现effect方法可以自执行其传入的函数参数 - 依赖收集,通过两个
Map结构和一个Set结构来映射数据关系,将所有的fn存放到dep中。通过一个全局变量reactiveEffect来获取到effct实例,为后续触发依赖时,直接拿dep中每一项去执行。 - 触发依赖,通过映射关系获取到
dep,因为dep是Set结构,可迭代,循环每项执行。
相关文章:
reactive和effect,依赖收集触发依赖
通过上一篇文章已经初始化项目,集成了ts和jest。本篇实现Vue3中响应式模块里的reactive方法。 前置知识要求 如果你熟练掌握Map, Set, Proxy, Reflect,可直接跳过这部分。 Map Map是一种用于存储键值对的集合,并且能够记住键的原始插入顺…...
【C#学习】backgroundWorker控件
BackgroundWorker 控件的几个实例(C# backgroundworker使用方法): 在 WinForms 中,有时要执行耗时的操作,在该操作未完成之前操作用户界面,会导致用户界面停止响应。 解决的方法就是新开一个线程ÿ…...
Istio学习笔记-部署模型
参考:Istioldie 1.18 / 部署模型 当您将 Istio 用于生产环境部署时,需要确定一系列的问题。 网格将被限制在单个集群中还是分布在多个集群中? 是将所有服务都放置在单个完全连接的网络中,还是需要网关来跨多个网络连接服务&#…...
磁盘调度算法
磁盘调度算法是计算机操作系统中用于管理磁盘上的数据访问的重要组成部分。这些算法有助于优化数据的读写操作,以减少磁盘访问时间,提高系统性能。以下是一些常见的磁盘调度算法: 先来先服务(FCFS,First-Come-First-Se…...
力扣题库2. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外,这两个数都不会以 0 开…...
【Linux】第十六站:进程地址空间
文章目录 一、程序地址空间1.内存的分布2.static修饰后为什么不会被释放3.一个奇怪的现象 二、进程地址空间1.前面现象的原因2.地址空间究竟是什么?3.为什么要有进程地址空间4.页表5.什么叫进程?6.进程具有独立性。为什么?怎么做到呢…...
基于Springboot的影城管理系统(有报告)。Javaee项目,springboot项目。
演示视频: 基于Springboot的影城管理系统(有报告)。Javaee项目,springboot项目。 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 项目介绍…...
如何在面试中胜出?接口自动化面试题安排上
📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢交流讨论:欢迎加入我们一起学习!📢资源分享:耗时200小时精选的「软件测试」资…...
联邦学习研究综述笔记
联邦学习 联邦学习的定义:联邦学习是一种分布式机器学习架构,包含多个客户端(参与者)和一个聚合服务器。客服端(参与方):在本地使用自己的私有数据训练模型,训练完成之后将模型的参…...
RedisTemplate乱码问题
其实这是在解决一个项目问题是发现的,因为原开发者的大意,造成了系统出现严重的逻辑问题。 因为系统系统采用分模块开发,某模块使用Spring提供的RedisTemplate进行值的读写,另一位使用了框架基于Jedis的一套公用方法进行值的读写…...
Java用户和内核交互图
...
2023.11.14使用bootstrap制作一个简洁的前端注册登录页
2023.11.14使用bootstrap制作一个简洁的前端注册登录页 比较简洁的登录页,主要是为自己开发的一些平台页面做测试用,前端具备功能如下: (1)输入用户名、密码,需补充后端验证代码。 (2ÿ…...
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博客 第二篇 《随机点名答题系统》——题库管理详解(类抽奖系统、在线答题…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
springboot 百货中心供应链管理系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,百货中心供应链管理系统被用户普遍使用,为方…...
苍穹外卖--缓存菜品
1.问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大 2.实现思路 通过Redis来缓存菜品数据,减少数据库查询操作。 缓存逻辑分析: ①每个分类下的菜品保持一份缓存数据…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...
LLMs 系列实操科普(1)
写在前面: 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容,原视频时长 ~130 分钟,以实操演示主流的一些 LLMs 的使用,由于涉及到实操,实际上并不适合以文字整理,但还是决定尽量整理一份笔…...
Unity中的transform.up
2025年6月8日,周日下午 在Unity中,transform.up是Transform组件的一个属性,表示游戏对象在世界空间中的“上”方向(Y轴正方向),且会随对象旋转动态变化。以下是关键点解析: 基本定义 transfor…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...
【51单片机】4. 模块化编程与LCD1602Debug
1. 什么是模块化编程 传统编程会将所有函数放在main.c中,如果使用的模块多,一个文件内会有很多代码,不利于组织和管理 模块化编程则是将各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用函数声明,其他.c文…...
linux设备重启后时间与网络时间不同步怎么解决?
linux设备重启后时间与网络时间不同步怎么解决? 设备只要一重启,时间又错了/偏了,明明刚刚对时还是对的! 这在物联网、嵌入式开发环境特别常见,尤其是开发板、树莓派、rk3588 这类设备。 解决方法: 加硬件…...
