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…...
【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
【根据当天日期输出明天的日期(需对闰年做判定)。】2022-5-15
缘由根据当天日期输出明天的日期(需对闰年做判定)。日期类型结构体如下: struct data{ int year; int month; int day;};-编程语言-CSDN问答 struct mdata{ int year; int month; int day; }mdata; int 天数(int year, int month) {switch (month){case 1: case 3:…...
调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...
STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...
