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

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响应式系统(一)

一、副作用函数。 副作用函数指的是会产生副作用的函数。例如&#xff1a;effect函数会直接或间接影响其他函数的执行&#xff0c;这时我们便说effect函数产生了副作用。 function effect(){document.body.innerText hello vue3 } 再例如&#xff1a; //全局变量let val 2f…...

MStart | MStart开发与学习

MStart | MStart开发与学习 1.学习 1.MStart |开机LOG显示异常排查及调整...

GoZero微服务个人探索之路(一)Etcd:context deadline exceeded原因探究及解决

产生错误原因就是与etcd交互时候需要指定&#xff1a; 证书文件的路径 客户端证书文件的路径 客户端密钥文件的路径 &#xff08;同时这貌似是强制默认就需要指定了&#xff09; 但我们怎么知道这三个文件路径呢&#xff0c;如下方法 1. 找到etcd的配置文件&#xff0c;里…...

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计算机本地日期和时间?

本文教程&#xff0c;主要介绍&#xff0c;在java中如何修改windows计算机本地日期和时间。 目录 一、程序代码 二、运行结果 一、程序代码 package com;import java.io.IOException;/**** Roc-xb*/ public class ChangeSystemDate {public static void main(String[] args)…...

flink中的row类型详解

在Apache Flink中&#xff0c;Row 是一个通用的数据结构&#xff0c;用于表示一行数据。它是 Flink Table API 和 Flink DataSet API 中的基本数据类型之一。Row 可以看作是一个类似于元组的结构&#xff0c;其中包含按顺序排列的字段。 Row 的字段可以是各种基本数据类型&…...

漏洞复现-Yearning front 任意文件读取漏洞(附漏洞检测脚本)

免责声明 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直接或者间接的…...

K8S中SC、PV、PVC的理解

存储类&#xff08;StorageClass&#xff09;定义了持久卷声明&#xff08;PersistentVolumeClaim&#xff09;所需的属性和行为&#xff0c;而持久卷&#xff08;PersistentVolume&#xff09;是实际的存储资源&#xff0c;持久卷声明&#xff08;PersistentVolumeClaim&#…...

Agisoft Metashape 基于影像的外部点云着色

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

图解结算平台:准确高效给商户结款

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

修改和调试 onnx 模型

1. onnx 底层实现原理 1.1 onnx 的存储格式 ONNX 在底层是用 Protobuf 定义的。Protobuf&#xff0c;全称 Protocol Buffer&#xff0c;是 Google 提出的一套表示和序列化数据的机制。使用 Protobuf 时&#xff0c;用户需要先写一份数据定义文件&#xff0c;再根据这份定义文…...

不同整数的最少数目和单词直接最短距离

写是为了更好的思考&#xff0c;坚持写作&#xff0c;力争更好的思考。 今天分享两个关于“最小、最短”的算法题&#xff0c;废话少说&#xff0c;show me your code&#xff01; 一、不同整数的最少数目 给你一个整数数组arr和一个整数k。现需要从数组中恰好移除k个元素&…...

【Microsoft Edge】版本 109.0.1518.55 (正式版本) (64 位) 更新失败解决方案

Microsoft Edge 版本号 109.0.1518.55&#xff08;正式版本&#xff09;&#xff08;64位&#xff09; 更新直接报错 检查更新时出错: 无法创建该组件(错误代码 3: 0x80040154 – system level) 问题出现之前 之前电脑日常硬盘百分百&#xff08;删文件和移动文件都慢得像…...

深度学习笔记(四)——使用TF2构建基础网络的常用函数+简单ML分类实现

文中程序以Tensorflow-2.6.0为例 部分概念包含笔者个人理解&#xff0c;如有遗漏或错误&#xff0c;欢迎评论或私信指正。 截图和程序部分引用自北京大学机器学习公开课 TF2基础常用函数 1、张量处理类 强制数据类型转换&#xff1a; a1 tf.constant([1,2,3], dtypetf.floa…...

大模型学习篇(一):初识大模型

目录 一、大模型的定义 二、大模型的基本原理与特点 三、大模型的分类 四、大模型的相关落地产品 五、总结 一、大模型的定义 大模型是指具有数千万甚至数亿参数的深度学习模型。大模型具有以下特点&#xff1a; 参数规模庞大&#xff1a;大模型的一个关键特征是其包含了…...

uni-app的学习【第二节】

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

matlab行操作快?还是列操作快?

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

基于SSM的流浪动物救助站

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…...

任务13:使用MapReduce对天气数据进行ETL(获取各基站ID)

任务描述 知识点&#xff1a; 天气数据进行ETL 重 点&#xff1a; 掌握MapReduce程序的运行流程熟练编写MapReduce程序使用MapReduce进行ETL 内 容&#xff1a; 编写MapReduce程序编写Shell脚本&#xff0c;获取MapReduce程序的inputPath将生成的inputPath文件传入到Wi…...

@Controller层自定义注解拦截request请求校验

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

SCAU期末笔记 - 数据分析与数据挖掘题库解析

这门怎么题库答案不全啊日 来简单学一下子来 一、选择题&#xff08;可多选&#xff09; 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘&#xff1a;专注于发现数据中…...

VTK如何让部分单位不可见

最近遇到一个需求&#xff0c;需要让一个vtkDataSet中的部分单元不可见&#xff0c;查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行&#xff0c;是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示&#xff0c;主要是最后一个参数&#xff0c;透明度…...

基于matlab策略迭代和值迭代法的动态规划

经典的基于策略迭代和值迭代法的动态规划matlab代码&#xff0c;实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...

Kafka入门-生产者

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

Windows安装Miniconda

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

C# 表达式和运算符(求值顺序)

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

LabVIEW双光子成像系统技术

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

永磁同步电机无速度算法--基于卡尔曼滤波器的滑模观测器

一、原理介绍 传统滑模观测器采用如下结构&#xff1a; 传统SMO中LPF会带来相位延迟和幅值衰减&#xff0c;并且需要额外的相位补偿。 采用扩展卡尔曼滤波器代替常用低通滤波器(LPF)&#xff0c;可以去除高次谐波&#xff0c;并且不用相位补偿就可以获得一个误差较小的转子位…...

热烈祝贺埃文科技正式加入可信数据空间发展联盟

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

边缘计算网关提升水产养殖尾水处理的远程运维效率

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