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] = value
        return 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);

    // +++updater
    user.age++;
    expect(nextAge).toBe(12);
  });
});

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

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

引用官方的例子:

let A0 = 1
let A1 = 2
let A2 = A0 + A1

console.log(A2) // 3

A0 = 2
console.log(A2) // 仍然是 3

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

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

let A2

function 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 -> dep
  let depMap = targetMap.get(target);
  if (!depMap) { // init
    depMap = new Map();
    targetMap.set(target, depMap);
  }
  let dep = depMap.get(key);
  if (!dep) { // init
    dep = 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结构,可迭代,循环每项执行。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

谈谈一个IT杂家的职业生涯规划,你的护城河被AI 攻破了么

文章大纲 没有顶会的从业者:成为深度学习老中医AIGC 还未能克服的难点:忽然的惊喜 -- 大模型的智能涌现未来还能做点什么,从计算机视觉的发展走向看T 字型人才与护城河成为更加熟练使用人工智能的人 参考文献与学习路径 我的职业生涯将近十年…

JVM实战-JVM之类加载时机

目录 JVM实战-JVM之类加载时机1 主动引用2 被动引用 JVM实战-JVM之类加载时机 Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机…

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

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

【luckfox】0、开发环境搭建

前言 本章简单介绍如何搭建luckfox的开发环境。 一、抓取luckfox源码 需要提前准备好ubuntu环境。 git clone https://github.com/LuckfoxTECH/luckfox-pico.git二、编译 youkaiubuntu:/home/luckfox/luckfox-pico$ ./build.sh lunchyoukaiubuntu:/home/luckfox/luckfox-p…

SoftwareTest6 - 用 Selenium 怎么点点点

用 Selenium 来点点点 一 . 什么是自动化 ?1.1 自动化测试的分类接口自动化测试UI 自动化测试 (界面测试) 1.2 实现自动化测试的工具 : selenium环境部署驱动 二 . selenium 的使用2.1 一个简单的示例 : 让谷歌浏览器在百度首页搜索蔡徐坤准备工作编写代码 2.2 打开谷歌浏览器…

C++内存分区 代码区 全局区 栈区 堆区

一.内存四区 1.1 代码区: 存放函数体的二进制代码,由操作系统进行管理。1.2 全局区: 存放全局变量,静态变量以及常量。1.3 栈区: 由编译器自动分配释放,存放函数的参数值,局部变量等。1.4 堆…

JVM虚拟机:垃圾回收器之G1

本文重点 在前面的课程中我们介绍了六个垃圾回收器,分别是新生代的三个以及老年代的三个,本文我们将介绍一个垃圾回收器,它既可以用于新生代又可以用于老年代,这个垃圾回收器就是G1。 G1垃圾回收器的特点 G1是一种服务器端的并发收集垃圾回收器,应用在多处理器和大容量…

大文件分片上传、断点续传、秒传

小文件上传 后端&#xff1a;SpringBootJDK17 前端&#xff1a;JavaScriptsparkmd5.min.js 一、依赖 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.2</ve…

python数据处理作业4:使用numpy数组对象,随机创建4*4的矩阵,并提取其对角元素

每日小语 真理诚然是一个崇高的字眼&#xff0c;然而更是一桩崇高的业绩。如果人的心灵与情感依然健康&#xff0c;则其心潮必将为之激荡不已。——黑格尔 难点&#xff1a;如何创建&#xff1f;取对角元素的函数是什么&#xff1f; gpt代码学习 import numpy as np# 随机创…

【数据结构】树与二叉树(十六):二叉树的基础操作:插入结点(算法Insert)

文章目录 5.2.1 二叉树二叉树性质引理5.1&#xff1a;二叉树中层数为i的结点至多有 2 i 2^i 2i个&#xff0c;其中 i ≥ 0 i \geq 0 i≥0。引理5.2&#xff1a;高度为k的二叉树中至多有 2 k 1 − 1 2^{k1}-1 2k1−1个结点&#xff0c;其中 k ≥ 0 k \geq 0 k≥0。引理5.3&…

【MySQL系列】 第四章 · 约束

写在前面 Hello大家好&#xff0c; 我是【麟-小白】&#xff0c;一位软件工程专业的学生&#xff0c;喜好计算机知识。希望大家能够一起学习进步呀&#xff01;本人是一名在读大学生&#xff0c;专业水平有限&#xff0c;如发现错误或不足之处&#xff0c;请多多指正&#xff0…

windows系统pycharm程序通过urllib下载权重https报错解决

报错内容&#xff1a; raise URLError(unknown url type: %s % type) urllib.error.URLError: <urlopen error unknown url type: https> 解决办法记录&#xff1a; 1. 下载 pyopenssl : pip install pyopenssl 此时&#xff0c; import ssl 可以通过提示指导你安…

机器视觉工程师,实际上调机仔需要居多,不需要那么多会机器视觉开发的,实际上机器视觉公司根本养不起

不要机器视觉开发等着倒闭&#xff0c;要那么多机器视觉开发是想倒闭&#xff0c;根本养不起。 人力对于机器视觉企业来说&#xff0c;仅仅是成本&#xff0c;也可以是剥削利润。当机器视觉公司开发一款标准软件后&#xff0c;意味着什么&#xff1f;技术可以复制&#xff0c;粘…

efcore反向共工程,单元测试

1.安装efcore需要的nuget <PackageReference Include"Microsoft.EntityFrameworkCore" Version"6.0.24" /> <PackageReference Include"Microsoft.EntityFrameworkCore.SqlServer" Version"6.0.24" /> <PackageRefere…

【每日一题】K 个元素的最大和

文章目录 Tag题目来源解题思路方法一&#xff1a;贪心 其他语言Cpython3 写在最后 Tag 【贪心】【脑筋急转弯】【数组】【2023-11-15】 题目来源 2656. K 个元素的最大和 解题思路 方法一&#xff1a;贪心 从第一次操作开始每次选择数组中的最大值&#xff0c;由于最大值在…

pycharm pro v2023.2.4(Python编辑开发)

PyCharm2023是一款集成开发环境&#xff08;IDE&#xff09;&#xff0c;专门为Python编程语言设计。以下是PyCharm2023的一些主要功能和特点&#xff1a; 代码编辑器&#xff1a;PyCharm2023提供了一个功能强大的代码编辑器&#xff0c;支持语法高亮、自动补全、代码调试、版…

后端接口性能优化分析-程序结构优化

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…

微信小程序 解决tab页切换过快 数据出错问题

具体问题&#xff1a;切换tab页切换过快时,上一个列表接口未响应完和当前列表数据冲突 出现数据错误 具体效果如下&#xff1a; 解决方式&#xff1a;原理 通过判断是否存在request 存在中断 并发送新请求 不存在新请求 let shouldAbort false; // 添加一个中断标志 let re…

Unity可视化Shader工具ASE介绍——10、ASE实现曲面细分

阿赵的Unity可视化Shader工具ASE介绍目录   大家好&#xff0c;我是阿赵。   之前介绍地面交互的时候&#xff0c;介绍了曲面细分着色器的使用。这个过程&#xff0c;在ASE里面也是可以实现的。关于曲面细分的具体作用&#xff0c;这里就不再重复&#xff0c;如果有兴趣了解…

8.指令格式,指令的寻址方式

目录 一. 指令格式 二. 扩展操作码 三. 指令寻址 &#xff08;1&#xff09;指令寻址 &#xff08;2&#xff09;数据寻址 1.直接寻址 2.间接寻址 3.寄存器寻址 4.寄存器间接寻址 5.隐含寻址 6.立即寻址 7.基址寻址 8.变址寻址 9.相对寻址 10.堆栈寻址 一. 指令…