Vue3.js“非原始值”响应式实现基本原理笔记(二)
如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~
作者:前端小王hs
阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主
此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来
书籍:《Vue.js设计与实现》 作者:霍春阳
本篇博文将在书第5.1节至5.4节的基础上进一步总结所提到的基础概念,附加了测试的代码运行示例,方便正在学习Vue3或想分析Vue3源码的朋友快速阅读
如有帮助,不胜荣幸
前文:
Vue3.js“非原始值”响应式实现基本原理笔记(一)
如何代理Object
“读取”是一个很宽泛的概念(原文)
在之前的笔记中,只简单的讨论了如obj.foo
这般获取对象属性值的读取,但读取有很多种,例如下面几种:
- 访问属性:obj.foo
- 判断:key in obj
- 遍历:for (const key in obj)
- …
这一章的内容,就是针对这几种不同的读取,以及其他的常见行为如删除等进行拦截
实现的逻辑主要是看操作符对应的拦截函数
在书中是通过查阅ECMA规范,明确操作符运行逻辑,进而找到操作符的运算结果是调用什么抽象方法,然后通过这个抽象方法找到对应的内部方法,进而对比Vue3.js“非原始值”响应式实现基本原理笔记(一)提到的Proxy内部方法表,选取对应的方法拦截
拦截 in
下面仅以in
操作符为例,我们来看一下书中是如何逐步找到拦截方法的
在ECMA-262规范的13.10.1(原文),找到in
操作符的运行时逻辑:
- 让 lref 的值为 RelationalExpression 的执行结果。
- 让 lval 的值为 ? GetValue(lref)。
- 让 rref 的值为 ShiftExpression 的执行结果。
- 让 rval 的值为 ? GetValue(rref)。
- 如果 Type(rval) 不是对象,则抛出 TypeError 异常。
- 返回 ? HasProperty(rval, ?
ToPropertyKey(lval))。
关键是第6步,出现了HasProperty()
,然后在在ECMA-262规范的7.3.11(原文)找到关于这个方法的逻辑:
- 断言:Type(O) 是 Object。
- 断言:IsPropertyKey§ 是 true。
- 返回 ? O.[[HasProperty]] §。
可以发现这个内部方法[[HasProperty]],然后在表中找到对应的拦截函数——has
,如下图所示:
然后就可以在Proxy
的handler
中使用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
的运行时逻辑为例):
RelationalExpression
:关系表达式,例如<
、>
、=
、in
、instanceof
、<=
、>=
等操作符,在这里指的操作符in
的左侧的表达式**ShiftExpression**
:位移操作的表达式,操作符in
的右侧的表达式lref
和lval
:RelationalExpression
会生成一个引用lref
,然后通过GetValue(lref)
得到lval
rref
和lref
:逻辑同上
拦截 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
是传入target
和key
,代码如下:
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_KEY
,iterate
的意思是重复
为什么传入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去考虑的不同情况
总结
这篇笔记主要复习了不同读取情况下的响应式实现:
- 如何拦截in操作符
- 如何拦截for…in操作符
- 当新增、修改和删除时如何实现响应式
相关文章:

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

论文 | PRCA: 通过可插拔奖励驱动的上下文适配器拟合用于检索问答的黑盒大语言模型
论文全称:PRCA: Fitting Black-Box Large Language Models for Retrieval Question Answering via Pluggable Reward-Driven Contextual Adapter 核心问题:如何在检索增强式问答(ReQA)任务中,利用大型语言模型…...
网络状态的智能感知:WebKit 支持 Network Information API 深度解析
网络状态的智能感知:WebKit 支持 Network Information API 深度解析 在现代 Web 应用中,理解用户的网络连接状态对于提供适应性体验至关重要。Network Information API,一个新兴的 Web API,允许 Web 应用访问设备的网络信息&…...

Vue3基础知识:组合式API中的provide和inject,他们作用是什么?如何使用?以及案例演示
1.provide和inject相较于父子传递的不同在于provide,inject可以用于跨层级通信(通俗易懂的讲就是可以实现爷孙之间的直接信息传递)。 1.跨层级传递数据 1.在顶层组件通过provide函数提供数据 2.底层组件通过inject函数获取数据 演示一:跨…...
Transformer自注意力机制(Self-Attention)模型
上一篇我们介绍了transform专题一:Seq2seq model,也知道了transfrom属于seq2seq模型,这一排篇咱们接着介绍另外几种seq2seq架构的模型。)RNN(循环神经网络)CNN(卷积神经网络)&…...

【计算机体系结构】缓存的false sharing
在介绍缓存的false sharing之前,本文先介绍一下多核系统中缓存一致性是如何维护的。 目前主流的多核系统中的缓存一致性协议是MESI协议及其衍生协议。 MESI协议 MESI协议的4种状态 MESI协议有4种状态。MESI是4种状态的首字母缩写,缓存行的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 # 创建虚拟环境,已有可跳过…...
docker 设置代理,通过代理服务器拉取镜像
docker 拉取目标镜像需要通过代理服务器进行时,可以通过为 docker 配置全局代理来实现。 注:Linux 上通过临时命令 export HTTP_PROXY 设置的代理,对 curl 这些有用,但是对 docker pull 不起作用。 示例 假设您的代理服务器地址是…...

OpenCV教程02:图像处理系统1.0(翻转+形态学+滤波+缩放+旋转)
-------------OpenCV教程集合------------- Python教程99:一起来初识OpenCV(一个跨平台的计算机视觉库) OpenCV教程01:图像的操作(读取显示保存属性获取和修改像素值) OpenCV教程02:图像处理…...
人工智能在招投标领域的运用---监控视频连续性检测
作者:舒城县公共交易中心 zhu_min726126.com 原创,转载请注明出处。 摘要 随着人工智能(AI)技术的飞速发展,其在各个领域的应用日益广泛。本文旨在探讨人工智能在招投标领域的运营,重点介绍AI对视频完整…...

加装德国进口高精度主轴 智能手机壳「高质量高效率」钻孔铣槽
在当前高度智能化的社会背景下,智能手机早已成为人们生活、工作的必备品,智能手机壳作市场需求量巨大。智能手机壳的加工过程涉及多个环节,包括钻孔和铣槽等。钻孔要求精度高、孔位准确,而铣槽则需要保证槽位规整、深度适宜。这些…...
Java Stream API 常用操作技巧
Java 8 引入的 Stream API 为集合操作提供了一种声明式编程模型,极大地简化了数据处理的复杂性。本文将介绍 Java Stream API 的几种常用操作方式,帮助开发者更高效地处理集合数据。 1. 过滤(Filtering) 过滤是选择集合中满足特…...

SwiftData 模型对象的多个实例在 SwiftUI 中不能及时同步的解决
概览 我们已经知道,用 CoreData 在背后默默支持的 SwiftUI 视图在使用 @FetchRequest 来查询托管对象集合时,若查询结果中的托管对象在别处被改变将不会在 FetchedResults 中得到及时的刷新。 那么这一“囧境”在 SwiftData 里是否也会“卷土重来”呢?空说无益,就让我们在…...
Android 系统网络、时间服务器配置修改
1.修改wifi 是否可用的检测地址: 由于编译的源码用的是谷歌的检测url,国内访问不了,系统会认为wifi网络受限,所以改成国内的地址 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这样的字符串,如何将对应的数据转换成geometry数据,实现如下 UPDATE coordinates SET geom ST_GeomFromText(POINT( || split_part(coord_str, , 1) || || split_part(coord_str, , 2) || ), 43…...

SQLServer Manager Studio扩展开发从入门到弃坑
Visualstudio的已经开发好了,可这个就是不行,直接运行点这些按钮加载失败,而我直接不调试模式,则直接什么都没有,调试 发现是根本没触发逻辑的。 文档资料太少, 我换了几个ssms.exe都不行,18-20…...

ComfyUI预处理器ControlNet简单介绍与使用(附件工作流)
简介 ControlNet 是一个很强的插件,提供了很多种图片的控制方式,有的可以控制画面的结构,有的可以控制人物的姿势,还有的可以控制图片的画风,这对于提高AI绘画的质量特别有用。接下来就演示几种热门常用的控制方式 1…...
【篇三】在vue3上实现阿里云oss文件直传
之前写了两篇关于文件上传的文章 【篇一】使用springbootvue实现阿里云oss上传 【篇二】使用springbootvue实现阿里云oss文件直传,解决大文件分片上传问题 今天介绍一下在vue3中实现阿里云oss文件直传,主要是基于篇二中的源码进行修改,看具体…...
OceanBase v4.2 特性解析:对Json与Xml的扩展支持
1. 背景 OceanBase的Oracle模式当前已实现对XMLType类型的支持,不仅包含了基本的构造、查询、更新以及格式转换功能,还支持使用Xpath查询从XML数据中提取特定值。在V 4.2.2 版本中,我们进一步扩展了Oracle模式下对XMLType的支持,…...
模型参数、模型存储精度、参数与显存
模型参数量衡量单位 M:百万(Million) B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...

cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...

2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...

网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
MySQL 部分重点知识篇
一、数据库对象 1. 主键 定义 :主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 :确保数据的完整性,便于数据的查询和管理。 示例 :在学生信息表中,学号可以作为主键ÿ…...