Vue3响应式系统(一)
一、副作用函数。
副作用函数指的是会产生副作用的函数。例如:effect函数会直接或间接影响其他函数的执行,这时我们便说effect函数产生了副作用。
function effect(){document.body.innerText = 'hello vue3'
}
再例如:
//全局变量let val = 2function effect() {val = 2 //修改全局变量,产生副作用
}
二、响应式数据。
上代码:
const obj = { text: 'hello world' }function effect() {// effect 函数的执行会读取 obj.textdocument.body.innerText = obj.text
}
当前修改obj.text的值的时候,除了本身的发生变化之外,不会有任何其他反应。
若修改obj.text的值的时候,effect函数副作用函数自动重新执行,如果能实现这个目标,那么obj对象就是响应式数据。很显然,目前还不能实现,接下来我们将数据变成响应式数据。
三、响应式数据的基本实现。
接上思考:如何将数据变为响应式数据呢?
通过上面我们可以发现有两点:
1.当effect副作用函数执行时,触发obj.text的读取操作。
2.当修改obj.text的值的时候,出发obj.text的设置操作。
问题的关键:我们如何才能拦截一个对象属性的读取和设置操作。在ES2015之前只能通过Object.defineProperty函数实现,这也是Vue.js 2所采用的方式。在ES2015+中,我们可以使用Proxy代理对象来实现,这也是Vue.js 3所采用的方式。
采用Proxy来实现:
/*** 实现一个响应式 * @param { Object } bucket* @param { Object } data* @param { Function } effect* @param { Object } obj */
// 存储副作用函数的桶
const bucket = new Set()// 副作用函数
function effect() {console.log(obj.text)
}//原始数据
const data = { text: 'hello world' }
//对数据的代理
const obj = new Proxy(data, {//拦截读取操作get(target, key) {//将副作用函数加入到桶里bucket.add(effect)//返回属性值return target[key]},//拦截设置操作set(target, key, newVal) {//设置属性值target[key] = newVal//把副作用函数从桶里取出来并执行bucket.forEach(fun => fun())//返回 true 代表设置操作成功return true}
})
effect()setTimeout(() => {console.log('一秒后触发设置')obj.text = 'hello vue3'
},1000)
目前还存在许多缺陷,我们需要去掉通过名字来获取副作用函数的硬编码机制 。
四、实现一个完善的响应式系统。
1.解决副作用函数收集到桶里的硬编码机制——我们需要注册一个副作用函数的机制
/*** 注册副作用函数机制* @param {any} activeEffect* @param {Function} effect* @param {Object} obj1* @param {Object} data 用的是上面的*/
//用全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
function effect(fn) {// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffectactiveEffect = fn// 执行副作用函数fn()
}
const obj1 = new Proxy(data, {//拦截读取操作get(target, key) {//--将副作用函数加入到桶里//--bucket.add(effect)//++将activeEffect中存储的副作用函数收集到“桶”中if (activeEffect) {bucket.add(activeEffect)}//返回属性值return target[key]},//拦截设置操作set(target, key, newVal) {//设置属性值target[key] = newVal//把副作用函数从桶里取出来并执行bucket.forEach(fun => fun())//返回 true 代表设置操作成功return true}
})
effect(() => {console.log('run', obj1.text)}
)
setTimeout(() => {console.log('一秒后触发设置')obj1.notExist = 'hello vue3'
},1000)
存在问题:
没有在副作用函数与被操作的目标字段之间建立明确的联系。无论读取的是哪一个属性,都会把副作用函数收集到“桶”里。
2.解决上述问题。
首先分析一下注册副作用函数触发都存在哪些角色:
①obj1对象
②text字段
③使用副作用函数注册的函数
我们来为这三个角色建立一个树形关系。target表示obj1——代理对象所代理的原始对象;用key来表示text字段——被操作的字段名;effectFn表示被注册的副作用函数。这个联系建立起来后,就可以解决前文提到的问题了。
我们需要重新设计“桶”的数据结构,不能简单的去使用Set类型的数据作为“桶”,我们需要将Set桶改为WeakMap桶。
为啥要用到WeakMap,而不是Map?
因为WeakMap对key是弱引用,只有被引用的有价值的信息可以访问,没有被引用的信息就会被垃圾回收器回收。如果是Map,即使信息没有引用,垃圾回收器也不会去回收它,那么就会有很大机率导致内存溢出。
代码如下:
const bucketMap = new WeakMap()
const obj2 = new Proxy(data, {get(target, key) {// 没有activeEffect直接返回if(!activeEffect) return target[key]// 取出WeakMap桶里的值 target ===> keylet depsMap = bucketMap.get(target)// 如果不存在depsMap,那就新建Map与target建立联系if(!depsMap) {bucketMap.set(target, (depsMap = new Map()))}// key ===> effectFnlet deps = depsMap.get(key)if(!deps) {depsMap.set(key, deps = new Set())}// 注册副作用函数deps.add(activeEffect)return target[key]},set(target, key, newVal) {target[key] = newVal// 取targetconst depsMap = bucketMap.get(target)if(!depsMap) return// 根据key取副作用函数const effects = depsMap.get(key)// 执行副作用函数effect && effect.forEach(fn => fn())return true}
})
我们可以将activeEffect注册副作用函数机制单独封装到一个函数track中,表达追踪的含义。将触发副作用函数单独封装到trigger函数中。代码更改如下:
/*** 建立联系* @param { Object } bucketMap* @param { Object } obj2* @param { Function } track 追踪* @param { Function } trigger 触发*/
// WeakMap桶
const bucketMap = new WeakMap()
function track(target, key) {// 没有activeEffect直接返回if (!activeEffect) return target[key]// 取出WeakMap桶里的值 target ===> keylet depsMap = bucketMap.get(target)// 如果不存在depsMap,那就新建Map与target建立联系if (!depsMap) {bucketMap.set(target, (depsMap = new Map()))}// key ===> effectFnlet deps = depsMap.get(key)if (!deps) {depsMap.set(key, deps = new Set())}// 注册副作用函数deps.add(activeEffect)
}
function trigger(target, key) {// 取targetconst depsMap = bucketMap.get(target)if (!depsMap) return// 根据key取副作用函数const effects = depsMap.get(key)// 执行副作用函数effect && effect.forEach(fn => fn())
}
const obj2 = new Proxy(data, {get(target, key) {// 注册副作用函数track(target, key)return target[key]},set(target, key, newVal) {target[key] = newVal// 触发副作用函数trigger(target, key)return true}
})
五、分支切换与cleanup
我们用以下代码说明分支切换。如下:
const data = { ok: true, text: 'hello world'}
const obj = new Proxy(/*....*/)
effect(function effectFn{document.body.innerText = obj.ok ? obj.text : 'not'
})
在effectFn函数内部的三元表达式,根据ok字段值的不同会执行不同的代码分支。ok的值发生变化时,代码执行的分支会根治变化,这就是所谓的分支切换。
分支切换可能会产生一流的副作用函数。根据上面的代码案例来说,effectFn与响应式数据建立的关系如下:

当修改ok字段值改为false的时候,text的不会被读取,所以指挥触发ok字段的读取,而不会触发text读取,所以理想状态下effectFn不应该被字段text所对应的依赖集合收集。
显然我们目前还不能做到这一点。

遗留的副作用函数会导致不必要的更新。解决问题的思路就是:每次副作用函数执行时,我们可以先把它从所有与之关联的依赖几何中删除。当副作用函数执行完毕后,会重新建立联系,新的联系里不会包含遗留的副作用函数。
重新设计effectFn函数
function effect(fn) {const effectFn = () => {activeEffect = effectFnfn()}// deps用来存储所有与这副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}
修改 track 追踪函数
function track(target, key) {if (!activeEffect) return target[key]let depsMap = bucketMap.get(target)if (!depsMap) {bucketMap.set(target, (depsMap = new Map()))}let deps = depsMap.get(key)if (!deps) {depsMap.set(key, deps = new Set())}deps.add(activeEffect)// ======= 主要就是增加关联数组中 ===========activeEffect.deps.push(deps)
}

有了这个联系后,我们就可以在每次副作用函数执行时,根据deps获取所有相关联的依赖集合,进而将副作用函数从依赖集合中移除。
function effect(fn) {const effectFn = () => {cleanup(effectFn)activeEffect = effectFnfn()}// deps用来存储所有与这副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}
function cleanup(effectFn) {//遍历effectFn的deps数组for(let i = 0; i < effectFn.deps.length; i++) {let deps = effectFn.deps[i]deps.delete(effectFn)}// 最后需要重置effectFn.deps数组effectFn.deps.length = 0
}
现在我们来执行一下这完整代码,看会有啥效果:
const data1 = {ok: true,text: 'hello world'
}
const obj2 = new Proxy(data1, {get(target, key) {// 注册副作用函数track(target, key)return target[key]},set(target, key, newVal) {target[key] = newVal// 触发副作用函数trigger(target, key)return true}
})function trigger(target, key) {// 取targetconst depsMap = bucketMap.get(target)if (!depsMap) return// 根据key取副作用函数const effects = depsMap.get(key)// 执行副作用函数effects && effects.forEach(fn => fn())
}/*** 重新设计effectFn* @param { Function } effect* @param { Function } cleanup* @param { Function } track
*/
function effect(fn) {const effectFn = () => {cleanup(effectFn)activeEffect = effectFnfn()}// deps用来存储所有与这副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}
function cleanup(effectFn) {//遍历effectFn的deps数组for(let i = 0; i < effectFn.deps.length; i++) {let deps = effectFn.deps[i]deps.delete(effectFn)}// 最后需要重置effectFn.deps数组effectFn.deps.length = 0
}function track(target, key) {// 没有activeEffect直接返回if (!activeEffect) return target[key]// 取出WeakMap桶里的值 target ===> keylet depsMap = bucketMap.get(target)// 如果不存在depsMap,那就新建Map与target建立联系if (!depsMap) {bucketMap.set(target, (depsMap = new Map()))}// key ===> effectFnlet deps = depsMap.get(key)if (!deps) {depsMap.set(key, deps = new Set())}// 注册副作用函数deps.add(activeEffect)// ======= 主要就是增加关联数组中 ===========activeEffect.deps.push(deps)
}// 测试
effect(() => {let n = obj2.ok ? obj2.text : 'not' console.log('run', n)}
)
setTimeout(() => {console.log('一秒后触发设置')obj2.ok = false
},1000)
可以看到目前会无限不断去执行, 问题出现在哪里呀?问题便出现在trigger函数下面这句中。
effects && effects.forEach(fn => fn())
Why?有啥问题?来看下面代码:
const set = new Set([1])set.forEach(item => {set.delete(1)set.add(1)console.log('遍历中!!!')
})
·
由于不断执行,我截不下全图。不断执行的原因:语言规范中说过,在调用forEach遍历Set集合时,一个值被访问过了,但被删除后又被重新添加到集合,如果此时forEach遍历没有结束,那么该值会重新被访问。所以,上面代码会不断去执行 。
同理,trigger函数里面的effects也是一样,当副作用函数执行的时候,cleanup会进行清除,但是副作用函数的执行会导致其被重新收集到集合中,而此时遍历仍然在进行,所以我们实现的响应式才会不断的去执行。
如何更改无限循环呢
我们可以构造另一个Set集合并遍历它。我们去修改一下trigger触发函数:
function trigger(target, key) {// 取targetconst depsMap = bucketMap.get(target)if (!depsMap) return// 根据key取副作用函数const effects = depsMap.get(key)// 执行副作用函数const effectToRun = new Set(effects) //新增effectToRun && effectToRun.forEach(fn => fn()) //新增// effects && effects.forEach(fn => fn()) //剔除
}
如上图所示,无限循环问题得以解决。
Vue响应式系统(二)
相关文章:

Vue3响应式系统(一)
一、副作用函数。 副作用函数指的是会产生副作用的函数。例如:effect函数会直接或间接影响其他函数的执行,这时我们便说effect函数产生了副作用。 function effect(){document.body.innerText hello vue3 } 再例如: //全局变量let val 2f…...
MStart | MStart开发与学习
MStart | MStart开发与学习 1.学习 1.MStart |开机LOG显示异常排查及调整...
GoZero微服务个人探索之路(一)Etcd:context deadline exceeded原因探究及解决
产生错误原因就是与etcd交互时候需要指定: 证书文件的路径 客户端证书文件的路径 客户端密钥文件的路径 (同时这貌似是强制默认就需要指定了) 但我们怎么知道这三个文件路径呢,如下方法 1. 找到etcd的配置文件,里…...

C语言从入门到实战——结构体与位段
结构体与位段 前言一、结构体类型的声明1.1 结构体1.1.1 结构的声明1.1.2 结构体变量的创建和初始化 1.2 结构的特殊声明1.3 结构的自引用 二、 结构体内存对齐2.1 对齐规则2.2 为什么存在内存对齐2.3 修改默认对齐数 三、结构体传参四、 结构体实现位段4.1 什么是位段4.2 位段…...

java如何修改windows计算机本地日期和时间?
本文教程,主要介绍,在java中如何修改windows计算机本地日期和时间。 目录 一、程序代码 二、运行结果 一、程序代码 package com;import java.io.IOException;/**** Roc-xb*/ public class ChangeSystemDate {public static void main(String[] args)…...
flink中的row类型详解
在Apache Flink中,Row 是一个通用的数据结构,用于表示一行数据。它是 Flink Table API 和 Flink DataSet API 中的基本数据类型之一。Row 可以看作是一个类似于元组的结构,其中包含按顺序排列的字段。 Row 的字段可以是各种基本数据类型&…...

漏洞复现-Yearning front 任意文件读取漏洞(附漏洞检测脚本)
免责声明 文章中涉及的漏洞均已修复,敏感信息均已做打码处理,文章仅做经验分享用途,切勿当真,未授权的攻击属于非法行为!文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直接或者间接的…...

K8S中SC、PV、PVC的理解
存储类(StorageClass)定义了持久卷声明(PersistentVolumeClaim)所需的属性和行为,而持久卷(PersistentVolume)是实际的存储资源,持久卷声明(PersistentVolumeClaim&#…...

Agisoft Metashape 基于影像的外部点云着色
Agisoft Metashape 基于影像的外部点云着色 文章目录 Agisoft Metashape 基于影像的外部点云着色前言一、添加照片二、对齐照片三、导入外部点云四、为点云着色五、导出彩色点云前言 本教程介绍了在Agisoft Metashape Professional中,将照片中的真实颜色应用于从不同源获取的…...

图解结算平台:准确高效给商户结款
这是《百图解码支付系统设计与实现》专栏系列文章中的第(4)篇。 本章主要讲清楚支付系统中商户结算涉及的基本概念,产品架构、系统架构,以及一些核心的流程和相关领域模型、状态机设计等。 1. 前言 收单结算是支付系统最重要的子…...

修改和调试 onnx 模型
1. onnx 底层实现原理 1.1 onnx 的存储格式 ONNX 在底层是用 Protobuf 定义的。Protobuf,全称 Protocol Buffer,是 Google 提出的一套表示和序列化数据的机制。使用 Protobuf 时,用户需要先写一份数据定义文件,再根据这份定义文…...
不同整数的最少数目和单词直接最短距离
写是为了更好的思考,坚持写作,力争更好的思考。 今天分享两个关于“最小、最短”的算法题,废话少说,show me your code! 一、不同整数的最少数目 给你一个整数数组arr和一个整数k。现需要从数组中恰好移除k个元素&…...
【Microsoft Edge】版本 109.0.1518.55 (正式版本) (64 位) 更新失败解决方案
Microsoft Edge 版本号 109.0.1518.55(正式版本)(64位) 更新直接报错 检查更新时出错: 无法创建该组件(错误代码 3: 0x80040154 – system level) 问题出现之前 之前电脑日常硬盘百分百(删文件和移动文件都慢得像…...

深度学习笔记(四)——使用TF2构建基础网络的常用函数+简单ML分类实现
文中程序以Tensorflow-2.6.0为例 部分概念包含笔者个人理解,如有遗漏或错误,欢迎评论或私信指正。 截图和程序部分引用自北京大学机器学习公开课 TF2基础常用函数 1、张量处理类 强制数据类型转换: a1 tf.constant([1,2,3], dtypetf.floa…...
大模型学习篇(一):初识大模型
目录 一、大模型的定义 二、大模型的基本原理与特点 三、大模型的分类 四、大模型的相关落地产品 五、总结 一、大模型的定义 大模型是指具有数千万甚至数亿参数的深度学习模型。大模型具有以下特点: 参数规模庞大:大模型的一个关键特征是其包含了…...

uni-app的学习【第二节】
四 路由配置及页面跳转 (1)路由配置 uni-app页面路由全部交给框架统一管理,需要在pages.json里配置每个路由页面的路径以及页面样式(类似小程序在app.json中配置页面路由) 接着第一节的文件,在pages里面新建三个页面 将之前的首页替换为下面的内容,其他页面如下图 然…...

matlab行操作快?还是列操作快?
在MATLAB中,通常情况下,对矩阵的列进行操作比对行进行操作更有效率。这是因为MATLAB中内存是按列存储的,因此按列访问数据会更加连续,从而提高访问速度。 一、实例代码 以下是一个简单的测试代码, % 测试矩阵大小 ma…...

基于SSM的流浪动物救助站
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:Vue 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目:是 目录…...

任务13:使用MapReduce对天气数据进行ETL(获取各基站ID)
任务描述 知识点: 天气数据进行ETL 重 点: 掌握MapReduce程序的运行流程熟练编写MapReduce程序使用MapReduce进行ETL 内 容: 编写MapReduce程序编写Shell脚本,获取MapReduce程序的inputPath将生成的inputPath文件传入到Wi…...

@Controller层自定义注解拦截request请求校验
一、背景 笔者工作中遇到一个需求,需要开发一个注解,放在controller层的类或者方法上,用以校验请求参数中(不管是url还是body体内,都要检查,有token参数,且符合校验规则就放行)是否传了一个token的参数&am…...

SCAU期末笔记 - 数据分析与数据挖掘题库解析
这门怎么题库答案不全啊日 来简单学一下子来 一、选择题(可多选) 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘:专注于发现数据中…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...
基于matlab策略迭代和值迭代法的动态规划
经典的基于策略迭代和值迭代法的动态规划matlab代码,实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...

Windows安装Miniconda
一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...

LabVIEW双光子成像系统技术
双光子成像技术的核心特性 双光子成像通过双低能量光子协同激发机制,展现出显著的技术优势: 深层组织穿透能力:适用于活体组织深度成像 高分辨率观测性能:满足微观结构的精细研究需求 低光毒性特点:减少对样本的损伤…...

永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器
一、原理介绍 传统滑模观测器采用如下结构: 传统SMO中LPF会带来相位延迟和幅值衰减,并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF),可以去除高次谐波,并且不用相位补偿就可以获得一个误差较小的转子位…...

热烈祝贺埃文科技正式加入可信数据空间发展联盟
2025年4月29日,在福州举办的第八届数字中国建设峰会“可信数据空间分论坛”上,可信数据空间发展联盟正式宣告成立。国家数据局党组书记、局长刘烈宏出席并致辞,强调该联盟是推进全国一体化数据市场建设的关键抓手。 郑州埃文科技有限公司&am…...

边缘计算网关提升水产养殖尾水处理的远程运维效率
一、项目背景 随着水产养殖行业的快速发展,养殖尾水的处理成为了一个亟待解决的环保问题。传统的尾水处理方式不仅效率低下,而且难以实现精准监控和管理。为了提升尾水处理的效果和效率,同时降低人力成本,某大型水产养殖企业决定…...