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结构,可迭代,循环每项执行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/143521.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React Virtual DOM及Diff算法

JSX到底是什么 使用React就一定会写JSX,JSX到底是什么呢?它是一种JavaScript语法的扩展,React使用它来描述用户界面长成什么样子,虽然它看起来非常像HTML,但他确实是javaScript,在React代码执行之前&#…

Kafka简单汇总

Kafka的结构图 多个Parttion共同组成这个topic的所有消息。每个consumer都属于一个consumer group,每条消息只能被consumer group中的一个Consumer消费, 但可以被多个consumer group消费。即组间数据是共享的,组内数据是竞争的。二、消费模型…

传输层协议-UDP协议

目录 传输层再谈端口号端口号范围划分认识知名端口号 UDP协议UDP协议格式UDP数据封装UDP数据分用 UDP协议的特点面向数据报 UDP缓冲区UDP使用注意事项基于UDP的应用层协议 传输层 实际上我们应用层的数据并不是直接发给网络的,而是需要先将数据发送给传输层&#xf…

指针传2

几天没有写博客了,怎么说呢?这让我总感觉缺点什么,心里空落落的,你懂吧! 好了,接下来开始我们今天的正题! 1. ⼆级指针 我们先来看看代码: 首先创建了一个整型变量a,将…

一题带你写出图论算法模板!!!

这题是道基础的图论算法题目 注释很重要!!!!!!! 在做这道题之前,我们先了解一下基础的图论算法吧!!! 1.floyd: 这样可以求出所有点…

【java学习—十四】java动态代理(6)

文章目录 1. 相关概念2. 步骤3. 举例 问题: 假设一个java项目有100个java类,每个java有10个方法,这总共有1000个方法,现在有这样一个需求,需要在每个java方法加上2句话:在方法执行前输出这个方法开始执行&a…

Qt 线程串口

文章目录 ui设置创建线程函数初始串口run函数接收发送数据读取数据处理读取的数据写入数据写入启动的命令 主线程 ui设置 创建线程函数 #include <QObject> #include <QThread> #include <QSerialPort> #include <QSerialPortInfo>class SerialPort :…

【Python】Matplotlib-多张图像的显示

一&#xff0c;情景描述 大家在写论文或者实验报告的时候&#xff0c;经常会放多张图片或数据图像在一起形成对比。比如&#xff0c;我现在有一张经过椒盐噪声处理的图像&#xff0c;现在进行三种滤波&#xff0c;分别是均值&#xff0c;高斯&#xff0c;中值滤波&#xff0c;…

ElementUI表格el-table自适应高度(表头表尾固定不动)

ElementUI表格el-table自适应高度&#xff08;表头表尾固定不动&#xff09;&#xff0c;内容只在中间滚动&#xff0c;效果如图&#xff1a; 实现代码 <div class"mt-10" :style"{height:tableHeight}"><div class"operation-bar">…

VScode+python开发,多个解释器切换问题

内容&#xff1a;主要VScode使用多个解释器 环境准备 VScode编辑器&#xff0c;两个版本python解释器 python3.7.2 python3.11.6 问题&#xff1a; 目前我们的电脑安装了python3.7.2、python3.11.6两个解释器&#xff0c;在vscode编辑器中&#xff0c;无法切换解释器使用如…

【蓝桥杯软件赛 零基础备赛20周】第3周——填空题

报名明年4月蓝桥杯软件赛的同学们&#xff0c;如果你是大一零基础&#xff0c;目前懵懂中&#xff0c;不知该怎么办&#xff0c;可以看看本博客系列&#xff1a;备赛20周合集 20周的完整安排请点击&#xff1a;20周计划 文章目录 00. 2023年第14届参赛数据0. 上一周答疑1. 填空…

RequestContextHolder详解

最近遇到的问题是在service获取request和response,正常来说在service层是没有request的,然而直接从controlller传过来的话解决方法太粗暴,后来发现了SpringMVC提供的RequestContextHolder遂去分析一番,并借此对SpringMVC的结构深入了解一下,后面会再发文章详细分析源码 1.Reque…

anaconda中安装pytorch和TensorFlow环境并在不同环境中安装kernel

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

单链表的实现

CSDN主页&#xff1a;醋溜马桶圈_C语言进阶,初始C语言,数据结构-CSDN博客 Gitee主页&#xff1a;mnxcc (mnxcc) - Gitee.com 专栏&#xff1a;数据结构_醋溜马桶圈的博客-CSDN博客 目录 1.认识单链表 2.创建单链表 3.单链表的操作 3.1打印单链表 3.2开辟新空间 3.3尾插 3.4头插…

深入探讨Linux中的文本文件查看命令

目录 前言1 cat命令2 less命令3 more命令4 head命令5 tail命令6 总结 前言 在Linux系统中&#xff0c;文本文件是日常工作中不可或缺的一部分&#xff0c;无论是配置文件、日志文件还是代码文件&#xff0c;都需要用到文本文件查看命令。在本文中&#xff0c;我们将深入研究一…

AI创作系统ChatGPT源码+AI绘画系统+支持OpenAI DALL-E3文生图,可直接对话文生图

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。新增支…

桶装水订水小程序app,线上预约订水更便捷

桶装水订水小程序app&#xff0c;线上预约订水更便捷。设置好地址&#xff0c;一键订水&#xff0c;工作人员送水到家。还能配送新鲜果蔬&#xff0c;绿色健康有保证。送水软件手机版&#xff0c;提供各种品牌桶装水&#xff0c;在线发起订水服务&#xff0c;由服务人员送水到家…

QT windows与linux之间sokcet通信中文乱码问题解决方法

QT windows与linux之间sokcet通信中文乱码问题解决方法 linux发送与接收都转码utf-8: tcpClient ->write( send_msg.toUtf8());//解决乱码&#xff0c;发送转码 接收&#xff1a; QByteArray buffer tcpClient->readAll(); if(!buffer.isEmpty()) { // ui->plain…

双十一买高画质投影仪,当贝F6还是极米H6?

如果你想购买一台4K画质的投影仪,那么在各大平台搜索“双十一最值得买的4K投影仪”时,一定会注意到当贝F6和极米H6这两个型号投影仪。个人认为当贝F6和极米H6都分别是当贝和极米两个品牌非常具有性价比的4K投影仪。那么到底哪一台更适合你。 首先放一张参数对比图,方便参数控研…

登上CMMLU性能评测榜单第一 四大维度解码夸克自研大模型

11月14日&#xff0c;拥有千亿参数的夸克自研大模型正式发布&#xff0c;立刻占据CMMLU榜单第一名。夸克大模型将应用于通用搜索、医疗健康、教育学习、职场办公等多个场景。性能方面&#xff0c;其整体水平已经超过GPT-3.5&#xff0c;其中在写作、考试等部分场景中可以超过GP…