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

Vue3.js“非原始值”响应式实现基本原理笔记(二)

如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~

作者:前端小王hs

阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主

此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来

书籍:《Vue.js设计与实现》 作者:霍春阳

本篇博文将在书第5.1节5.4节的基础上进一步总结所提到的基础概念,附加了测试的代码运行示例,方便正在学习Vue3想分析Vue3源码的朋友快速阅读

如有帮助,不胜荣幸

前文:
Vue3.js“非原始值”响应式实现基本原理笔记(一)

如何代理Object

“读取”是一个很宽泛的概念(原文)

在之前的笔记中,只简单的讨论了如obj.foo这般获取对象属性值的读取,但读取有很多种,例如下面几种:

  1. 访问属性:obj.foo
  2. 判断:key in obj
  3. 遍历:for (const key in obj)

这一章的内容,就是针对这几种不同的读取,以及其他的常见行为如删除等进行拦截

实现的逻辑主要是看操作符对应的拦截函数

在书中是通过查阅ECMA规范,明确操作符运行逻辑,进而找到操作符的运算结果是调用什么抽象方法,然后通过这个抽象方法找到对应的内部方法,进而对比Vue3.js“非原始值”响应式实现基本原理笔记(一)提到的Proxy内部方法表,选取对应的方法拦截

拦截 in

下面in操作符为例,我们来看一下书中是如何逐步找到拦截方法

ECMA-262规范的13.10.1(原文),找到in操作符的运行时逻辑:

  1. 让 lref 的值为 RelationalExpression 的执行结果。
  2. 让 lval 的值为 ? GetValue(lref)。
  3. 让 rref 的值为 ShiftExpression 的执行结果。
  4. 让 rval 的值为 ? GetValue(rref)。
  5. 如果 Type(rval) 不是对象,则抛出 TypeError 异常。
  6. 返回 ? HasProperty(rval, ?
    ToPropertyKey(lval))。

关键是第6步,出现了HasProperty(),然后在在ECMA-262规范的7.3.11(原文)找到关于这个方法的逻辑:

  1. 断言:Type(O) 是 Object。
  2. 断言:IsPropertyKey§ 是 true。
  3. 返回 ? O.[[HasProperty]] §。

可以发现这个内部方法[[HasProperty]],然后在表中找到对应的拦截函数——has,如下图所示:

image.png

然后就可以在Proxyhandler中使用has进行拦截了,代码如下:

const obj = { foo: 1 }
const p = new Proxy(obj, {has(target, key) {track(target, key)return Reflect.has(target, key)}
})effect(() => {'foo' in p // 将会建立依赖关系
})

第5章有非常多的关于ECMA的运行时逻辑,在书中没有解释,所以笔者在这里还是简单介绍一下一些关键词(以上述in的运行时逻辑为例):

  1. RelationalExpression:关系表达式,例如<>=ininstanceof<=>=等操作符,在这里指的操作符in左侧的表达式
  2. **ShiftExpression**:位移操作的表达式,操作符in右侧的表达式
  3. lreflvalRelationalExpression会生成一个引用lref,然后通过GetValue(lref)得到lval
  4. rreflref:逻辑同上

拦截 for…in(遍历所有可枚举属性)

逻辑和in是相同的,找规范找到最后发现可以使用ownKey()去拦截

但是在ownKey中会做一些处理,代码如下:

const obj = { foo: 1 }
const ITERATE_KEY = Symbol()const p = new Proxy(obj, {ownKeys(target) {// 将副作用函数与 ITERATE_KEY 关联track(target, ITERATE_KEY)return Reflect.ownKeys(target)}
})

注:这本书的特点就是类似于电视连续剧,整一章节的内容是不断累积的,所以的时候不要间断,同时要多复习

在之前的笔记中,track是传入targetkey,代码如下:

function track(target, key) {// 没有 activeEffect,直接 returnif (!activeEffect) return;let depsMap = bucket.get(target);if (!depsMap) {bucket.set(target, (depsMap = new Map()));}let deps = depsMap.get(key);if (!deps) {depsMap.set(key, (deps = new Set()));}const array = Array.from(deps);deps.add(activeEffect);activeEffect.deps.push(deps);
}

现在变成了传入ITERATE_KEYiterate的意思是重复

为什么传入ITERATE_KEY?

因为ownKeys是用来获取一个对象的所有键,不是与任何具体的进行绑定,所以就定义一个ITERATE_KEY作为标识,去与副作用函数进行绑定

那么当触发的时候,也同样需要传入ITERATE_KEY,代码如下:

trigger(target, ITERATE_KEY)

这里需要注意的是,遍历是遍历,触发拦截是触发拦截,整个过程只会触发一次ownKeys,可以理解为执行到for..in时就触发拦截,然后for...in循环遍历自身可枚举的属性

添加属性对for…in的影响

在上述代码中,当执行p.bar=2时,for...in就会由循环一次变为两次(因为obj变为了两个),但此时不会触发与ITERATE_KEY关联的副作用函数

原因非常简单,执行p.bar=2,关联的是与bar相关联的副作用函数

解决的方法也非常简单,在trigger中把两者都添加到effectsToRun

function trigger(target, key) {const depsMap = bucket.get(target)if (!depsMap) return// 取得与 key 相关联的副作用函数const effects = depsMap.get(key)// 取得与 ITERATE_KEY 相关联的副作用函数const iterateEffects = depsMap.get(ITERATE_KEY)const effectsToRun = new Set()// 将与 key 相关联的副作用函数添加到 effectsToRuneffects && effects.forEach(effectFn => {if (effectFn !== activeEffect) {effectsToRun.add(effectFn)}})// 将与 ITERATE_KEY 相关联的副作用函数也添加到effectsToRuniterateEffects && iterateEffects.forEach(effectFn => {if (effectFn !== activeEffect) {effectsToRun.add(effectFn)}})effectsToRun.forEach(effectFn => {if (effectFn.options.scheduler) {effectFn.options.scheduler(effectFn)} else {effectFn()}})
}

这里的ITERATE_KEY是外部定义的Symbol

target即遍历的obj

修改属性对foo…in的影响

修改属性不会对foo...in产生影响,但需要注意的是修改属性新增属性使用的都是[[Set]],所以需要做个区分,代码如下:

const p = new Proxy(obj, {  // 拦截设置操作set(target, key, newVal, receiver) {// 如果属性不存在,则说明是在添加新属性,否则是设置已有属性const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD';// 设置属性值const res = Reflect.set(target, key, newVal, receiver);// 将 type 作为第三个参数传递给 trigger 函数trigger(target, key, type);return res;},// 省略其他拦截函数
});

然后再在trigger中进行判断,如果是ADD时才执行depsMap.get(ITERATE_KEY),代码如下:

function trigger(target, key, type) {  // 省略其他逻辑// 只有当操作类型为 'ADD' 时,才触发与 ITERATE_KEY 相关联的副作用函数  if (type === 'ADD') {  const iterateEffects = depsMap.get(ITERATE_KEY);  iterateEffects && iterateEffects.forEach(effectFn => {if (effectFn !== activeEffect) {effectsToRun.add(effectFn);}});}
}

在书中还提到了定义枚举类型的重要性:

const TriggerType = {SET: 'SET',ADD: 'ADD'
};

便于后期维护

删除属性对for…in的影响

在书中通过查阅规范,得知是通过deleteProperty去拦截的,所以代码如下:

const p = new Proxy(obj, {  deleteProperty(target, key) {  // 检查被操作的属性是否是对象自己的属性  const hadKey = Object.prototype.hasOwnProperty.call(target, key);  // 使用 Reflect.deleteProperty 完成属性的删除  const res = Reflect.deleteProperty(target, key);  if (res && hadKey) {  // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新  trigger(target, key, 'DELETE');  }  return res;  }  
});

这里手写是进行了一个判断,删除的属性是否属于自身,这是因为执行的逻辑可能是如delete p.a,执行了但是这个属性不存在,所以需要进行一个判断

那么最后就是在trigger中继续加多一个type判断,代码如下:

function trigger(target, key, type) {  // 省略其他逻辑// 只有当操作类型为 'ADD' 或 'DELETE' 时才触发 if (type === 'ADD' || type === 'DELETE') {  const iterateEffects = depsMap.get(ITERATE_KEY);  iterateEffects && iterateEffects.forEach(effectFn => {if (effectFn !== activeEffect) {effectsToRun.add(effectFn);}});}
}

其实看到这里,能够发现在设计时是从CURD去考虑的不同情况

总结

这篇笔记主要复习了不同读取情况下的响应式实现:

  1. 如何拦截in操作符
  2. 如何拦截for…in操作符
  3. 当新增、修改和删除时如何实现响应式

相关文章:

Vue3.js“非原始值”响应式实现基本原理笔记(二)

如果您觉得这篇文章有帮助的话&#xff01;给个点赞和评论支持下吧&#xff0c;感谢~ 作者&#xff1a;前端小王hs 阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主 此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来 书籍&a…...

论文 | PRCA: 通过可插拔奖励驱动的上下文适配器拟合用于检索问答的黑盒大语言模型

论文全称&#xff1a;PRCA: Fitting Black-Box Large Language Models for Retrieval Question Answering via Pluggable Reward-Driven Contextual Adapter 核心问题&#xff1a;如何在检索增强式问答&#xff08;ReQA&#xff09;任务中&#xff0c;利用大型语言模型&#xf…...

网络状态的智能感知:WebKit 支持 Network Information API 深度解析

网络状态的智能感知&#xff1a;WebKit 支持 Network Information API 深度解析 在现代 Web 应用中&#xff0c;理解用户的网络连接状态对于提供适应性体验至关重要。Network Information API&#xff0c;一个新兴的 Web API&#xff0c;允许 Web 应用访问设备的网络信息&…...

Vue3基础知识:组合式API中的provide和inject,他们作用是什么?如何使用?以及案例演示

1.provide和inject相较于父子传递的不同在于provide,inject可以用于跨层级通信&#xff08;通俗易懂的讲就是可以实现爷孙之间的直接信息传递&#xff09;。 1.跨层级传递数据 1.在顶层组件通过provide函数提供数据 2.底层组件通过inject函数获取数据 演示一&#xff1a;跨…...

Transformer自注意力机制(Self-Attention)模型

​ 上一篇我们介绍了transform专题一&#xff1a;Seq2seq model&#xff0c;也知道了transfrom属于seq2seq模型&#xff0c;这一排篇咱们接着介绍另外几种seq2seq架构的模型。&#xff09;RNN&#xff08;循环神经网络&#xff09;CNN&#xff08;卷积神经网络&#xff09;&…...

【计算机体系结构】缓存的false sharing

在介绍缓存的false sharing之前&#xff0c;本文先介绍一下多核系统中缓存一致性是如何维护的。 目前主流的多核系统中的缓存一致性协议是MESI协议及其衍生协议。 MESI协议 MESI协议的4种状态 MESI协议有4种状态。MESI是4种状态的首字母缩写&#xff0c;缓存行的4种状态分别…...

Ubuntu24.04 Isaacgym的安装

官方论坛 rl-接口 教程1 教程2 教程3 1.下载压缩包 link 2. 解压 tar -xvf IsaacGym_Preview_4_Package.tar.gz核心教程在 isaacgym/docs/install.html下 3. 从源码安装 Ubuntu24.04还需首先进入虚拟环境 python -m venv myenv # 创建虚拟环境&#xff0c;已有可跳过…...

docker 设置代理,通过代理服务器拉取镜像

docker 拉取目标镜像需要通过代理服务器进行时&#xff0c;可以通过为 docker 配置全局代理来实现。 注&#xff1a;Linux 上通过临时命令 export HTTP_PROXY 设置的代理&#xff0c;对 curl 这些有用&#xff0c;但是对 docker pull 不起作用。 示例 假设您的代理服务器地址是…...

OpenCV教程02:图像处理系统1.0(翻转+形态学+滤波+缩放+旋转)

-------------OpenCV教程集合------------- Python教程99&#xff1a;一起来初识OpenCV&#xff08;一个跨平台的计算机视觉库&#xff09; OpenCV教程01&#xff1a;图像的操作&#xff08;读取显示保存属性获取和修改像素值&#xff09; OpenCV教程02&#xff1a;图像处理…...

人工智能在招投标领域的运用---监控视频连续性检测

作者&#xff1a;舒城县公共交易中心 zhu_min726126.com 原创&#xff0c;转载请注明出处。 摘要 随着人工智能&#xff08;AI&#xff09;技术的飞速发展&#xff0c;其在各个领域的应用日益广泛。本文旨在探讨人工智能在招投标领域的运营&#xff0c;重点介绍AI对视频完整…...

加装德国进口高精度主轴 智能手机壳「高质量高效率」钻孔铣槽

在当前高度智能化的社会背景下&#xff0c;智能手机早已成为人们生活、工作的必备品&#xff0c;智能手机壳作市场需求量巨大。智能手机壳的加工过程涉及多个环节&#xff0c;包括钻孔和铣槽等。钻孔要求精度高、孔位准确&#xff0c;而铣槽则需要保证槽位规整、深度适宜。这些…...

Java Stream API 常用操作技巧

Java 8 引入的 Stream API 为集合操作提供了一种声明式编程模型&#xff0c;极大地简化了数据处理的复杂性。本文将介绍 Java Stream API 的几种常用操作方式&#xff0c;帮助开发者更高效地处理集合数据。 1. 过滤&#xff08;Filtering&#xff09; 过滤是选择集合中满足特…...

SwiftData 模型对象的多个实例在 SwiftUI 中不能及时同步的解决

概览 我们已经知道,用 CoreData 在背后默默支持的 SwiftUI 视图在使用 @FetchRequest 来查询托管对象集合时,若查询结果中的托管对象在别处被改变将不会在 FetchedResults 中得到及时的刷新。 那么这一“囧境”在 SwiftData 里是否也会“卷土重来”呢?空说无益,就让我们在…...

Android 系统网络、时间服务器配置修改

1.修改wifi 是否可用的检测地址&#xff1a; 由于编译的源码用的是谷歌的检测url,国内访问不了&#xff0c;系统会认为wifi网络受限&#xff0c;所以改成国内的地址 adb shell settings delete global captive_portal_https_urladb shell settings delete global captive_por…...

类和对象深入理解

目录 static成员概念静态成员变量面试题补充代码1代码2代码3如何访问private中的成员变量 静态成员函数静态成员函数没有this指针 特性 友元友元函数友元类 内部类特性1特性2 匿名对象拷贝对象时的一些编译器优化 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接…...

在postgres数据库中的几个简单用法

1、例如表中coord_str的字段数据是121.12334 31.3435这样的字符串&#xff0c;如何将对应的数据转换成geometry数据&#xff0c;实现如下 UPDATE coordinates SET geom ST_GeomFromText(POINT( || split_part(coord_str, , 1) || || split_part(coord_str, , 2) || ), 43…...

SQLServer Manager Studio扩展开发从入门到弃坑

Visualstudio的已经开发好了&#xff0c;可这个就是不行&#xff0c;直接运行点这些按钮加载失败&#xff0c;而我直接不调试模式&#xff0c;则直接什么都没有&#xff0c;调试 发现是根本没触发逻辑的。 文档资料太少&#xff0c; 我换了几个ssms.exe都不行&#xff0c;18-20…...

ComfyUI预处理器ControlNet简单介绍与使用(附件工作流)

简介 ControlNet 是一个很强的插件&#xff0c;提供了很多种图片的控制方式&#xff0c;有的可以控制画面的结构&#xff0c;有的可以控制人物的姿势&#xff0c;还有的可以控制图片的画风&#xff0c;这对于提高AI绘画的质量特别有用。接下来就演示几种热门常用的控制方式 1…...

【篇三】在vue3上实现阿里云oss文件直传

之前写了两篇关于文件上传的文章 【篇一】使用springbootvue实现阿里云oss上传 【篇二】使用springbootvue实现阿里云oss文件直传&#xff0c;解决大文件分片上传问题 今天介绍一下在vue3中实现阿里云oss文件直传&#xff0c;主要是基于篇二中的源码进行修改&#xff0c;看具体…...

OceanBase v4.2 特性解析:对Json与Xml的扩展支持

1. 背景 OceanBase的Oracle模式当前已实现对XMLType类型的支持&#xff0c;不仅包含了基本的构造、查询、更新以及格式转换功能&#xff0c;还支持使用Xpath查询从XML数据中提取特定值。在V 4.2.2 版本中&#xff0c;我们进一步扩展了Oracle模式下对XMLType的支持&#xff0c;…...

Ubuntu系统下交叉编译openssl

一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机&#xff1a;Ubuntu 20.04.6 LTSHost&#xff1a;ARM32位交叉编译器&#xff1a;arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...

MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例

一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)

概述 在 Swift 开发语言中&#xff0c;各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过&#xff0c;在涉及到多个子类派生于基类进行多态模拟的场景下&#xff0c;…...

为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?

在建筑行业&#xff0c;项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升&#xff0c;传统的管理模式已经难以满足现代工程的需求。过去&#xff0c;许多企业依赖手工记录、口头沟通和分散的信息管理&#xff0c;导致效率低下、成本失控、风险频发。例如&#…...

使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装

以下是基于 vant-ui&#xff08;适配 Vue2 版本 &#xff09;实现截图中照片上传预览、删除功能&#xff0c;并封装成可复用组件的完整代码&#xff0c;包含样式和逻辑实现&#xff0c;可直接在 Vue2 项目中使用&#xff1a; 1. 封装的图片上传组件 ImageUploader.vue <te…...

C++中string流知识详解和示例

一、概览与类体系 C 提供三种基于内存字符串的流&#xff0c;定义在 <sstream> 中&#xff1a; std::istringstream&#xff1a;输入流&#xff0c;从已有字符串中读取并解析。std::ostringstream&#xff1a;输出流&#xff0c;向内部缓冲区写入内容&#xff0c;最终取…...

全志A40i android7.1 调试信息打印串口由uart0改为uart3

一&#xff0c;概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本&#xff1a;2014.07&#xff1b; Kernel版本&#xff1a;Linux-3.10&#xff1b; 二&#xff0c;Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01)&#xff0c;并让boo…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...

Python 包管理器 uv 介绍

Python 包管理器 uv 全面介绍 uv 是由 Astral&#xff08;热门工具 Ruff 的开发者&#xff09;推出的下一代高性能 Python 包管理器和构建工具&#xff0c;用 Rust 编写。它旨在解决传统工具&#xff08;如 pip、virtualenv、pip-tools&#xff09;的性能瓶颈&#xff0c;同时…...