实现vue3响应式系统核心-合理触发响应

合理触发响应

简介

在上一篇文章中,我们增强了对对象的拦截,解决了以下问题:

  • 拦截 in操作符
  • 拦截 for in 循环
  • 拦截对象的删除操作

接下来我们在对响应式系统做一些优化,避免一些不必要的响应

代码地址: https://github.com/SuYxh/share-vue3

代码并没有按照源码的方式去进行组织,目的是学习、实现 vue3 响应式系统的核心,用最少的代码去实现最核心的能力,减少我们的学习负担,并且所有的流程都会有配套的图片,图文 + 代码,让我们学习更加轻松、快乐。

每一个功能都会提交一个 commit ,大家可以切换查看,也顺变练习练习 git 的使用。

新值与旧值相等

单元测试

it('newValue === oldValue', () => {const mockFn = vi.fn();const obj = reactive({ foo: 1, bar: NaN })effect(function effectFn() {mockFn()console.log(obj.foo);console.log(obj.bar);})expect(mockFn).toHaveBeenCalledTimes(1);obj.foo = 1expect(mockFn).toHaveBeenCalledTimes(1);obj.bar = NaNexpect(mockFn).toHaveBeenCalledTimes(1);
})

代码实现

// 拦截设置操作
set(target, key, newVal, receiver) {// 先获取旧值const oldVal = target[key]// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性const type = Object.prototype.hasOwnProperty.call(target, key)? TriggerType.SET: TriggerType.ADD;// 设置属性值const res = Reflect.set(target, key, newVal, receiver);// 较新值与旧值,只有当它们不全等,并且不都是 NaN 的时候才触发响应if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {trigger(target, key, type);}return res;
},

set 拦截函数内首先获取旧值 oldVal ,接着比较新值与旧值,只有当它们不全等的时候才触发响应。这里需要注意的是 NaN的问题:

NaN === NaN  // false
NaN !== NaN  // true

所以,需要在新值和旧值不全等的情况下,要保证它们都不是 NaN 。

原型继承属性

单元测试

it("原型继承属性", () => {const mockFn = vi.fn();const obj = {};const proto = { bar: 1 };const child = reactive(obj);const parent = reactive(proto);// 使用 parent 作为 child 的原型Object.setPrototypeOf(child, parent);effect(() => {mockFn()console.log(child.bar); // 1});expect(mockFn).toHaveBeenCalledTimes(1);// 修改 child.bar 的值child.bar = 2; // 会导致副作用函数重新执行两次expect(mockFn).toHaveBeenCalledTimes(2);});

执行单测看看:

image-20240122002711353

从单测可以看出:副作用函数不仅执行了,还执行了2次,这会造成不必要的更新。

问题分析

我们知道,如果对象自身不存在该属性,会往对象的原型上找。当读取child.bar属性值时,由于child代理的对象 obj自身没有bar属性,因此会获取对象obj的原型,也就是parent对象,所以最终得到的实际上是 parent.bar的值。

同时,parent本身也是响应式数据,因此在副作用函数中访问 parent.bar的值时,会导致副作用函数被收集,从而也建立响应联系。所以我们能够得出一个结论,即 child.barparent.bar都与副作用函数建立了响应联系。

如果设置的属性不存在于对象上,那么会取得其原型,并调用原型的 [[Set]] 方法,也就是 parent [[Set]]内部方法。由于 parent是代理对象,所以这就相当于执行了它的 set拦截函数。换句话说,虽然我们操作的是 child.bar,但这也会导致 parent代理对象的 set拦截函数被执行。

所以当parent代理对象的 set拦截函数执行时,就会触发副作用函数重新执行,这就是为什么修改 child.bar的值会导致副作用函数重新执行两次。

解决

既然执行两次,那么只要屏蔽其中一次不就可以了吗?

屏蔽掉原型上的那次副作用函数的重新执行,即 parent.bar触发的那次。

如何屏蔽呢?

通过 set函数的第三个参数 receiver 来进行区分:

// child 的 set 拦截函数
set(target, key, value, receiver) {// target 是原始对象 obj// receiver 是代理对象 child
}// parent 的 set 拦截函数
set(target, key, value, receiver) {// target 是原始对象 proto// receiver 仍然是代理对象 child
}

parent代理对象的set拦截函数执行时,此时target是原始对象proto,而receiver仍然是代理对象 child,而不再是target的代理对象。

代码实现

代理对象可以通过 raw属性读取原始数据:

其实这里最好我们应该通过 Symbol('raw') 的方式来进行定义,避免引起冲突。比如,target 中存在一个 raw 属性呢?

function reactive(obj) {return new Proxy(obj, {get(target, key, receiver) {// 代理对象可以通过 raw 属性访问原始数据if (key === 'raw') {return target;}track(target, key);return Reflect.get(target, key, receiver);}// 省略其他拦截函数});
}

有了它,我们就能够在 set 拦截函数中判断 receiver 是不是 target 的代理对象了:

function reactive(obj) {return new Proxy(obj, {set(target, key, newVal, receiver) {const oldVal = target[key];const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD';const res = Reflect.set(target, key, newVal, receiver);// target === receiver.raw 说明 receiver 就是 target 的代理对象if (target === receiver.raw) {if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {trigger(target, key, type);}}return res;}// 省略其他拦截函数});
}

运行单测

image-20240122002759676

运行测试

pnpm test

image-20240122002955600

引导扫码关注

一个前端小学生的学习之路,如果你喜欢前端,我们可以一起进行学习、交流、共建。可以添加好友,结伴学习,成长的路上不孤单!

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

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

相关文章

【Idea+Maven+Git:构建高效Java项目的强大组合】

引言 在当今的软件开发世界中,集成开发环境(IDE)、构建工具和版本控制系统是每个项目不可或缺的组成部分。本文将深入探讨这三个工具:IntelliJ IDEA、Maven和Git,以及它们如何协同工作,帮助开发者构建更高…

Kotlin快速入门系列8

Kotlin的泛型 与Java一样,Kotlin也提供泛型。泛型,即 "参数化类型",将类型参数化,可以用在类,接口,方法上。可以为类型安全提供保证,消除类型强转的烦恼。声明泛型类的格式如下&…

TortoiseSVN各版本汉化包下载

首先进入下载版本列表 1.下载地址:https://sourceforge.net/projects/tortoisesvn/files ​ 2.选择自己版本进入​ 3.选择Language Packs进入,选择对应语言包下载。 ​ 4.在TortoiseSVN根目录下点击安装即可。 ​

给准备从事软件开发工作的年轻人的13个建议

从事软件开发是一个不断学习和适应变化的过程。这里有一些针对刚入行或准备从事软件开发工作的年轻人的建议: 掌握基础知识:确保你有扎实的编程基础。了解至少一种编程语言的语法和核心概念,比如C语言、Python、Java或C#。同时,理…

第38期 | GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区,集成了生成预训练Transformer(GPT)、人工智能生成内容(AIGC)以及大型语言模型(LLM)等安全领域应用的知识。在这里,您可以…

Sqli靶场 11--->22Less

打靶场,打靶场,打靶场,打靶场......靶场你别打我 球球 11.不用密码(狂喜) 这一关知不知道账号密码都无所谓 那么我们就尝试一下报错类型,单引号报错,好,字符型 构造poc I_don_t_know_t…

QT自制软键盘 最完美、最简单、支持中文输入(二)

目录 一、前言 二、本自制虚拟键盘特点 三、中文输入原理 四、组合键输入 五、键盘事件模拟 六、界面 七、代码 7.1 frmKeyBoard 头文件代码 7.2 frmKeyBoard 源文件代码 八、使用示例 九、效果 十、结语 一、前言 由于系统自带虚拟键盘不一定好用,也不一…

力扣hot100 分割回文串 集合 dfs

Problem: 131. 分割回文串 文章目录 思路Code💖 DP预处理版 思路 👨‍🏫 参考题解 Code import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.List;public class Solution {int n;//字符…

vulnhub-DC-2

信息收集: kali里使用netdiscover发现主机 使用命令: nmap -sS -sV -A -n 172.16.5.18 打开网站 找到flag1 他提示我们使用cewl工具(一个字典生成工具) 那么既然我们有了密码 我们还需要知道用户名使用wpscan 这个工具使用命令&a…

PyTorch深度学习实战(34)——Pix2Pix详解与实现

PyTorch深度学习实战(34)——Pix2Pix详解与实现 0. 前言1. 模型与数据集1.1 Pix2Pix 基本原理1.2 数据集分析1.3 模型构建策略 2. 实现 Pix2Pix 生成图像小结系列链接 0. 前言 Pix2Pix 是基于生成对抗网络 (Convolutional Generative Adversarial Netwo…

Flask 入门2:路由

1. 前言 在上一节中&#xff0c;我们使用到了静态路由&#xff0c;即一个路由规则对应一个 URL。而在实际应用中&#xff0c;更多使用的则是动态路由&#xff0c;它的 URL是可变的。 2. 定义一个很常见的路由地址 app.route(/user/<username>) def user(username):ret…

测试 35 个 webshell 检测引擎的查杀结果

最近发现了一个有意思的 使用分支对抗技术制作的 PHP Webshell 开源项目&#xff0c;共数十个查杀引擎免杀&#xff0c;项目地址&#xff1a;https://github.com/icewolf-sec/PerlinPuzzle-Webshell-PHP 什么是 Webshell Webshell 是一种恶意脚本&#xff0c;它能让攻击者通过…