Vue3 reactive原理(一)-代理对象及数组
Proxy 只能拦截对一个对象的基本操作(例如读取、设置属性值),而无法拦截复合操作(例如,obj.fun(),由两个基本操作组成,1)get到fun这个属性,2)函数调用)。
1 代理对象
| 操作 | 跟踪方法 | 触发方法 |
| 访问属性,obj.name | get(traget,p) | set、delete |
| 判断对象或原型上是否存在给定的key, key in obj | has(target,p) | delete,set |
| 使用for...in遍历对象 | ownkeys(target) | delete,set(添加新属性时) |
表 普通对象所有可能的读取操作
ownkeys方法没有具体的属性,这时我们指定一个唯一符号:ITERATE_KEY。来将它与副作用函数关联。
const ITERATE_KEY = Symbol("ITERATE_KEY")
return new Proxy(obj,{// 省略其他代码set(target, p, newValue, receiver) {const type = Object.prototype.hasOwnProperty.call(target,p) ? 'SET' : 'ADD'const res = Reflect.set(target,p,newValue,receiver)trigger(target,p,type,newValue)return res},deleteProperty(target, p) {const res = Reflect.deleteProperty(target,p)trigger(target,p,'DELETE')return res },has(target, p) {track(target,p)return Reflect.has(target,p)},ownKeys(target) {track(target,ITERATE_KEY)return Reflect.ownKeys(target)}
})
function trigger(target,p,type,newValue) {const map = effectMap.get(target)if (map) {const addSet = new Set()// 省略其他代码if (type === 'ADD') {const tempSet = map.get(ITERATE_KEY)tempSet && tempSet.forEach(fn => {if (activeEffect !== fn) addSet.add(fn)})}addSet.forEach(fn => fn())}
}
1.1 合理触发
为了提供性能及用户交互,应当满足下列条件时,才触发响应:
- 值发生改变时。
- 初始值和新值不都是NaN。
- 响应对象的原型也说响应对象时,访问原型上的属性时,只触发一次。
return new Proxy(obj,{get(target, p, receiver) {if (p === 'raw') return target// 省略其他代码},set(target, p, newValue, receiver) {// 省略其他代码if (receiver.raw === target) {if (oldVal !== newValue && (newValue === newValue || oldVal === oldVal)) {trigger(target,p,type)}}},deleteProperty(target, p) {const hadKey = Object.prototype.hasOwnProperty.call(target,p)const res = Reflect.deleteProperty(target,p)if (hadKey && res) {trigger(target,p,'DELETE')}},
})
1.2 深响应与浅响应
目前的reactive函数创建的响应对象是浅响应,即对象的首层才具有响应性。如果对象的某个属性值是个对象,那么该对象不具备响应性。而深响应是指无论多少层,对象都具有响应性。
function createReactive(obj,isShallow = false) {if (obj.raw) return objreturn new Proxy(obj,{get(target, p, receiver) {// 省略其他代码const val = Reflect.get(target,p,receiver)if (isShallow) return valreturn typeof val === 'object' && val != null ? createReactive(val) : val},// 省略其他代码})
}
1.2.1 深只读和浅只读
某些数据要是只读的(例如props),当用户尝试修改只读数据时,会收到一条警告信息。浅只读是指,对象的首层不可写,但是其他层可写(对象的一个属性值也是对象时,那么这个属性值对象里的属性是可写的)。
function createReactive(obj,isShallow = false,isReadonly) {if (obj.raw) return objreturn new Proxy(obj,{get(target, p, receiver) {// 省略其他代码const val = Reflect.get(target,p,receiver)if (isShallow) return valreturn typeof val === 'object' && val != null ? (isReadonly ? readonly(val) : createReactive(val)) : val},set(target, p, newValue, receiver) {if (isReadonly) {console.error('该属性不可写')return}// 省略其他代码},deleteProperty(target, p) {if (isReadonly) {console.error('该属性不可写')return}// 省略其他代码},// 省略其他代码})
}function readonly(obj,isShallow = false) {return createReactive(obj,isShallow,true)
}
2 代理数组
数组也是一种对象(但属于异质对象,与常规对象相比,它们的部分内部方法和常规对象的不同)。因此用于代理普通对象的大部份代码可以继续使用。
2.1 索引与length
- 通过索引设置元素值时,可能会影响到length属性,即当设置索引值大等于数组长度时,length属性会发生改变。
- 设置length属性,可能会影响到索引值。当length设置为更小值时,索引大等于length的部分元素全部会被删除。
new Proxy(obj,{set(target, p, newValue, receiver) {// 省略其他代码const type = Array.isArray(target) ? (Number(p) < target.length ? 'SET' : 'ADD') :(Object.prototype.hasOwnProperty.call(target,p) ? 'SET' : 'ADD')// 省略其他代码},// 省略其他代码
})function trigger(target,p,type,newValue) {const map = effectMap.get(target)if (map) {// 省略其他代码if (type === 'ADD' && Array.isArray(target)) {const tempSet = map.get("length")tempSet && tempSet.forEach(fn => {if (activeEffect !== fn) addSet.add(fn)})}if (p === 'length' && Array.isArray(target)) {map.forEach((set,key) => {if (key >= newValue) {set.forEach(fn => {if (activeEffect !== fn) addSet.add(fn)})}})}addSet.forEach(fn => fn())}
}
2.2 遍历数组
1)for...in,和普通对象一样,内部会调用ownKeys方法,但不同的是,其触发条件是length的改变。
new Proxy(obj,{ownKeys(target) {track(target,Array.isArray(target) ? 'length' : ITERATE_KEY)return Reflect.ownKeys(target)}// 省略其他代码
})
2)for...of,索引值的设置及length的改变,都会触发该迭代,所以几乎不要添加额外的代码,就能让for...for迭代具有响应性。
从性能上及为了避免发生意外的错误,我们不应该使副作用函数与symbol值之间建立响应联系。
2.3 数组的查找方法
数组的方法内部其实都依赖了对象的基本语义。因此大多数情况下,我们不需要做特殊处理即可让这些方法按预期工作。但某些场景,执行结果会不符合我们预期:
const proxyObj = reactive(['hi','js',{name: 'js'}])console.log(proxyObj.includes('hi'),proxyObj.includes(proxyObj[2])) // true、false
这里,查找基本类型数据时,结果是正确的。但是查找对象时,结果错误。这是因为reactive函数会为属性中的对象也创建响应对象,而且每次都会创建新的响应对象。而且,这里还有两个问题:
- 将响应体对象赋值给另一个响应体时,reactive不应该为其再创建响应体了。
- 无论查找对象,还是其响应体对象,返回的结果应该一致(从业务的角度看,它们属于同一对象)。
const reactiveMap = new WeakMap()new Proxy(obj,{get(target, p, receiver) {if (p === 'raw') return target// 省略其他代码if (Array.isArray(target) && arrayInstrumentation.hasOwnProperty(p)) {return Reflect.get(arrayInstrumentation,p,receiver)}// 省略其他代码},
})function reactive(obj) {const originalProxy = reactiveMap.get(obj)if (originalProxy) return originalProxy;const proxyObj = createReactive(obj)reactiveMap.set(obj,proxyObj)return proxyObj
}const arrayInstrumentation = {};
['includes','indexOf','lastIndexOf'].forEach(method => {const originalMethod = Array.prototype[method]arrayInstrumentation[method] = function (...args) {let res = originalMethod.apply(this,args)if (res === false || res === -1) {res = originalMethod.apply(this.raw,args)}return res}
});
2.4 隐式修改数组长度的原型方法
push/pop/shift/unshift/splice这些方法会隐式地修改数组长度。例如push方法,在往数组中添加元素时,既会读取length,也会设置数组的length。这会导致两个独立的副作用函数互相影响:
effect(() => {proxyObj.push(1)
})
effect(() => {proxyObj.push(2)
})
运行上面这段代码,会得到栈溢出的错误。因为副作用函数1执行时,会修改及读取length,会触发副作用函数2执行,而副作用2也会修改和读取length。这样就好造成循环调用。
解决方法是:“屏蔽”这些方法对length属性的读取。
['push','pop','shift','unshift','splice'].forEach(method => {const originalMethod = Array.prototype[method]arrayInstrumentation[method] = function (...args) {shouldTrack = falseconst res = originalMethod.apply(this,args)shouldTrack = truereturn res}
})function track(target,p) {if (activeEffect && shouldTrack) {// 跟踪代码}
}相关文章:
Vue3 reactive原理(一)-代理对象及数组
Proxy 只能拦截对一个对象的基本操作(例如读取、设置属性值),而无法拦截复合操作(例如,obj.fun(),由两个基本操作组成,1)get到fun这个属性,2)函数调用)。 1 …...
基于联咏 NT98692芯片赋能边缘计算IP摄像机与XVR监控系统解决方案
联咏 NT98692 是一款新世代整合度极高的 SoC,具有高影像品质、低位元率、低功耗,针对 8Kp30 边缘运算 IP 摄影机与后端监控系统 XVR 应用。此 SoC 整合了 ARM Quad Cortex A73 CPU 核心、新一代 ISP 和 AI ISP、H.265/H.264 视讯压缩编解码器、DSP、高效…...
Python设计模式 - 工厂方法模式
定义 工厂方法模式是一种创建型设计模式,它定义一个创建对象的接口,让其子类来处理对象的创建,而不是直接实例化对象。 结构 抽象工厂(Factory):声明工厂方法,返回一个产品对象。具体工厂类都…...
学习记录:ESP32控制舵机 FREERTOS BLE
控制舵机 PWM信号 PWM信号是一种周期性变化的方波信号,它有两个关键参数: 周期(Period):一个完整的PWM信号的时间长度,通常用秒(s)或毫秒(ms)表示。占空比…...
react中的useState和Hook、副作用
react的组件分为类组件和函数组件,Hook 是一种特殊的函数,可以让你在函数组件中使用类组件中才有的一些特性。useState、useEffect、useReducer都是Hook。其中useState用于在函数组件中添加状态,useEffect用于在函数组件中执行副作用…...
Linux嵌入式学习——数据结构——线性表的链式结构
线性表的链式存储 解决顺序存储的缺点,插入和删除,动态存储问题。 特点: 线性表链式存储结构的特点是一组任意的存储单位存储线性表的数据元素,存储单元可以是连续的,也可以不连续。可以被存储在任意内存未被占…...
文本编辑 文本中的各种空格
参考资料 欧文の半角スペースは2種類ある!?无中断空格常见空格一览浅析什么是零宽度字符以及零宽度字符在实际中的应用场景空格象形字间隔无中断空格零宽间隔 目录 零. 各种空格在Notepad中的效果一. 半角空格二. 全角空格三. TAB空格四. 无中断空格4.1 定义4.2 H…...
Vue插槽 (Slots)详解
目录 前言基础插槽具名插槽作用域插槽默认插槽动态插槽名总结相关阅读 前言 Vue的插槽(Slots)是一个非常强大的特性,它允许你在组件的模板中嵌入父组件的内容。插槽使得组件之间的内容分发变得灵活,尤其在构建可复用组件时非常…...
Unity中有关Animation的一点笔记
也许更好的阅读体验 Animation Unity中Animation类并不是直接记载了和播放动画有关的信息,可以简单理解Animation为一个动画播放器,播放的具体内容就像卡带一样,当我们有了卡带后我们可以播放动画。 对应的则是编辑器中的组件 所以Anima…...
module federation模块联邦与微前端
module federation是什么 webpack5新增了module federation,module federation的作用,将每个构建(build)作为容器(这是一个概念),构建后的资源可以正常部署,同时还具备在运行时对外暴露其中的模块,这就意味着多个构建…...
日常开发记录分享——C#控件ToolTip实现分栏显示内容
文章目录 需求来源实现思路实施请看VCR等等别走,有优化 需求来源 需要在鼠标浮动到指定位置后提示出详细的信息,一开始使用的tooltip实现,但是里面的内容效果并不理想,需要有条理性,于是就想到能不能将展示的东西分列…...
Kettle下载安装
环境说明 虚拟机:Win7;MySql8.0 主机:Win11;JDK1.8;Kettle 9.4(Pentaho Data Integration 9.4)(下载方式见文末) 安装说明 【1】解压后运行Spoon.bat 【2】将jar包 复…...
最新版Golang pprof使用(引入、抓取、分析,图文结合)
最新版Golang pprof使用 🔥具体实践: Go调试神器pprof使用教程Golang线上内存爆掉问题排查(pprof) Github地址:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/go-demo/go-pprof 引入pprof:import _ “net/http/pprof” …...
vue3学习记录1:emit的写法
emit是用于child组件向parent组件通信的工具,因为vue3的script可以设置为setup,写法同vue2有较大区别。 一、script setup - 直接写 <script lang"ts" setup>const emit defineEmits([close]);function handleClose() {emit(close);}…...
Visual Studio Code + vue快速安装配置Node.js+Vue+webpack+vscode
第一部分:Node.js 第一步:下载Node.js 方法1:链接 下载 | Node.js 中文网 (nodejs.cn) 方法2:百度网盘 链接:https://pan.baidu.com/s/1zIqu8H9rb_I1i-1OWD7swQ?pwdaurk 提取码:aurk --来自百度网盘…...
【Dart 教程系列第 49 篇】什么是策略设计模式?如何在 Dart 中使用策略设计模式
这是【Dart 教程系列第 49 篇】,如果觉得有用的话,欢迎关注专栏。 博文当前所用 Flutter SDK:3.22.1、Dart SDK:3.4.1 文章目录 一:什么是策略设计模式?二:为什么要使用策略设计模式࿱…...
BGP路由反射器
原理概述 缺省情况下,路由器从它的一个 IBGP对等体那里接收到的路由条目不会被该路由器再传递给其他IBGP对等体,这个原则称为BGP水平分割原则,该原则的根本作用是防止 AS内部的BGP路由环路。因此,在AS内部,一般需要每台…...
DolphinDB Web 端权限管理:可视化操作指南
在现代数据库管理中,高效和直观的权限管理对于用户的数据安全是至关重要的。过去 DolphinDB 用户需要依赖系统脚本来管理用户和权限,这对于缺乏技术背景的管理员来说既复杂又容易出错。 为了提升用户体验和操作效率,DolphinDB 目前在 Web 上…...
学习Vue2收藏这一篇就够了(如何创建Vue实例)
什么是Vue? Vue是什么:是一个用于构建用户界面的渐进式框架 什么是构建用户界面:基于数据动态渲染页面 什么是渐进式:循序渐进的学习 什么是框架:一整套完整的项目解决方案 创建Vue实例 核心步骤(4步…...
Mysql数据库第四次作业
mysql> create table student(sno int primary key auto_increment,sname varchar(30) not null unique,Ssex varchar(2) check (Ssex男 or Ssex女) not null,Sage int not null,Sdept varchar(10) default计算机 not null); mysql> create table Course(Con int primar…...
学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...
GC1808高性能24位立体声音频ADC芯片解析
1. 芯片概述 GC1808是一款24位立体声音频模数转换器(ADC),支持8kHz~96kHz采样率,集成Δ-Σ调制器、数字抗混叠滤波器和高通滤波器,适用于高保真音频采集场景。 2. 核心特性 高精度:24位分辨率,…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
Mobile ALOHA全身模仿学习
一、题目 Mobile ALOHA:通过低成本全身远程操作学习双手移动操作 传统模仿学习(Imitation Learning)缺点:聚焦与桌面操作,缺乏通用任务所需的移动性和灵活性 本论文优点:(1)在ALOHA…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...
Qemu arm操作系统开发环境
使用qemu虚拟arm硬件比较合适。 步骤如下: 安装qemu apt install qemu-system安装aarch64-none-elf-gcc 需要手动下载,下载地址:https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x…...
