vue.js的设计与实现(响应系统2)

ops/2024/9/19 17:41:29/ 标签: vue.js, vue.js的设计与实现

文章目录

    • 概要
    • 分支切换与cleanup
    • 嵌套的effect与effect栈
    • 避免无限递归循环
    • 调度执行
    • 小结

概要

接上文,我们已经写出了基础的effect收集,但是还是会有些问题。这一篇,我们就是来解决这些问题的

分支切换与cleanup

首先,我们需要明确分钟切换的定义,看下以下代码:

const data = {text:'hello word',ok:true
}
const obj = new Proxy(data,{/*...*/})
effect(()=>{document.body.innerText = obj.ok?obj.text:'not'
})

当effectFn (effect(effectFn) effectFn是effect传参的函数) 函数内部是一个三元表达式的时候,会根据obj.ok的值执行不同的代码分支,当obj.ok的值发生变化时,代码执行的分支也会跟着变化,这就是所谓的分支切换

执行以上代码,我们发现当obj.ok的值时false的时候,修改obj.text的值。effectFn也会重新执行,这个肯定是有问题的,因为在effectFn中,我们走的分支并没有读取到obj.text的值。原因在于第一次执行的时候读取了obj.text的值,已经把effectFn对应的key值 text 放到了依赖收集的桶中,所以更改obj.text值的时候,就会取出effectFn执行。

我们已经知道原因了,那想必各位同学都有想法了吧?对的我们只要每次执行的副作用函数的时候,把它从之前的所有已关联的依赖集合中删除,当副作用函数执行完毕后,就会建立新的联系。这里就有一个问题,我们要怎么知道它是和哪些依赖集合中呢?所有我们修改effect函数和tarck函数,如以下代码:

let activeEffect
function effect(fn){const effectFn = ()=>{cleanup(effectFn)activeEffect = effectFnfn()}//effectFn新增属性deps,用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}
function cleanup(effectFn){for(let i =0 ;i <effectFn.deps.length;i++){const deps = effectFn.deps[i]deps.delete(effectFn)}effectFn.deps.length = 0
}
function tarck(target,key){if(!activeEffect) return const desMap = bucket.get(target)if(!desMap){bucket.set(target,desMap = new Map())}const deps = desMap.get(key)if(!deps){desMap.set(key,deps = new Set())}deps.add(activeEffect)activeEffect.deps.push(deps)
}

意思就是:我们在effectFn副作用函数上添加deps属性来记录所关联的依赖,在每次执行effectFn函数的时候,都会把effectFn.deps清空,我们可以看到在tarck函数的activeEffect.deps.push(deps)这句,deps是一个数组,数组里面存了deps这个Set的数据,Set是一个引用数据类型,所有push进入effectFn.deps的也是一个地址,这个地址指向了一个堆空间,我们不管在Set里面更改还是在effectFn.deps里面更改,更改的都是这个堆里面的内容。

我们执行上面代码,我们会发现,会一直无限循环,这是为什么呢?
我们看下下面这段代码:

const set = new Set([1])
set.forEach(item=>{set.delete(1)set.add(1)console.log('遍历中)
})

在语言规范中有明确的提到,在调用forEach遍历Set集合的时候,如果一个值被访问过,单该值被删除并重新添加到集合,如果此时forEach遍历还没有结束,就会重新访问,所以会进行无限递归。

那么问题又来了,要怎么解决呢?我们可以在用一个Set集合去遍历它,如以下代码:

const set = new Set([1])
const newSet = newSet(set)
newSet .forEach(item=>{set.delete(1)set.add(1)console.log('遍历中)
})

这里,同学们可以理解吗?set和newSet是两个不同的东西了,所以newSet遍历去更改set里面的值是对newSet没有任何影响的。所以就不会无限递归了

所以我们修改修改trigger函数里面的内容了,如下:

function trigger(target,key){const depsMap = bucket.get(target)if(!depsMap) return const effects = depsMap.get(key)const effectsToRun = new Set(effects)effectsToRun && effectsToRun.forEach(effectFn=>effectFn())
}

嵌套的effect与effect栈

我们可以设想一下,如果effect函数不支持嵌套,会出现什么的问题的:我们看下以下代码:

const data = {foo:true,bar:true}
const obj = new Proxy(data,{/*...*/})
let temp1,temp2
effect(function effectFn1(){console.log('effectFn1执行')effect(function effectFn2(){console.log('effectFn2执行')temp2 = obj.bar})temp1 = obj.foo
})

然后我们在修改一下 obj.foo的值,我们会发现 他执行的是 effectFn2,这是为什么呢?我们按照代码走下去:

  1. 创建了一个data对象
  2. 对data对象进行proxy代理
  3. 创建了两个变量
  4. 执行effect函数,这里我们要着重分析一下,我们先回顾一下 effect的函数内容,如下:
function effect(fn){const effectFn = ()=>{cleanup(effectFn)activeEffect = effectFnfn()}//effectFn新增属性deps,用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}

effect函数执行的时候 首先会把effectFn1函数放进来,activeEffect函数执行的就是effectFn1函数,到这里时没有问题,但是在fn执行的时候,我们就把effectFn2函数赋值给了activeEffect函数,这时候我们做了get操作,获取了obj.foo,此事,对应的activeEffect函数对应的effectFn2函数,所以foo这个key值对应的副作用函数就变成了effectFn2而不是effectFn1了。我们已经知道问题所在,那我们就把effect函数改造一下,如下:

//我们新建一个数组,来模拟栈
const  effectStack = []
function effect(fn){const effectFn = ()=>{cleanup(effectFn)effectStack.push(effectFn) activeEffect = effectFnfn()effectStack.pop()activeEffect = effectStack[effectStack.length -1]}//effectFn新增属性deps,用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}

我们在fn调用完成后 在effectStack里面弹出最后一个,再把activeEffect赋值effectStack的最后一项,这么做的目的就是为了保证 当前执行的key值的副作用函数,在执行get操作的时候,可以准确副作用函数添加到Set集合中,我们改完后再来执行一下的嵌套函数,我们再来分析一下:

  1. 创建了一个data对象
  2. 对data对象进行proxy代理
  3. 创建了两个变量
  4. 执行effect函数,先拿到effectFn1,把他加入到effectStack里面,然后执行effectfn1,然后我们就碰到了effectFn2,然后执行effectFn2,这时候,我们的activeEffect就会等于 effectFn2, 我们把effectFn2 push 到了effectStack里面,当effectFn2执行完成后,我们在effectStack弹出了effectFn2,把activeEffect赋值成effectFn1,我们在执行了obj.foo的get操作,我们就把key为foo的副作用函数对应到了effectFn1,这样就满足了我们的要求。

避免无限递归循环

我们在实现一个完美的响应式系统的时候,需要考虑到诸多的细节,我们试下下面的代码会造成什么样的情况

const data = { foo : 1}
const obj = new Proxy(data,{/*...*/})
effect(()=>{obj.foo++})

执行上面的代码,我们会发现栈溢出,这是为什么呢?obj.foo++ 是不是等于 obj.foo = obj.foo +1,我们就会发现一个问题,这个代码执行get操作也执行了set操作,当我们执行set操作的时候,又会把副作用函数拿出来执行一次,就会往复循环,我们会知道在vue中,这种代码的话,就是只会执行一次,那我们就往一次的方向想。

我们会发现 它执行set和get的时候 activeEffect指向的函数都是一样的,那我们是不是可以加一个守卫条件,当set的时候 如果副作用函数和activeEffect时一样的时候,我们就不触发呢?
我们来试下,直接修改trigger函数:

function trigger(target,key){const depsMap = bucket.get(target)if(!depsMap) return const effects = depsMap.get(key)//const effectsToRun = new Set(effects) //直接在遍历赋值的时候,把activeEffect给排除掉const effectsToRun = new Set()effects && effects.forEach((effectFn)=>{if(effectFn != activeEffect){effectsToRun.add(effectFn)}})effectsToRun && effectsToRun.forEach(effectFn=>effectFn())
}

这样我们就可以避免无限递归调用了

调度执行

可调度性也是响应式系统非常重要的的特性,首先我们要知道什么是可调度性。所谓的可调度性,是指trigger函数执行副作用函数的时候,可以决定副作用函数的执行时机、次数以及方式
我们看下以下代码:

const data = { foo : 1}
const obj = new Proxy(data,{/*...*/})
effect(()=>{console.log(obj.foo)
})
obj.foo++
console.log('结束了!')

这样代码输出结果是:

1
2
结束了

如果需求有变,我们输出的顺序要改成:

1
结束了
2

我们需要怎么不改变代码的顺序的情况下来改变执行结果呢?我们这个功能可以给用户自由调配。我们添加一个options来给用户自己配置,那我们需要更改以下代码了,如下:

effect(()=>{console.log(obj.foo)
},{scheduler(fn){//...}
})
function effect(fn,options={}){const effectFn = ()=>{cleanup(effectFn)effectStack.push(effectFn) activeEffect = effectFnfn()effectStack.pop()activeEffect = effectStack[effectStack.length -1]}effectFn.options = options //新增 把他挂载在副作用函数上//effectFn新增属性deps,用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}
//在tirgger函数更改以下
function tirgger(target,key){const depsMap = bucket.get(target)if(!depsMap) return const effects = depsMap.get(key)//const effectsToRun = new Set(effects) //直接在遍历赋值的时候,把activeEffect给排除掉const effectsToRun = new Set()effects && effects.forEach((effectFn)=>{if(effectFn != activeEffect){effectsToRun.add(effectFn)}})effectsToRun && effectsToRun.forEach(effectFn=>{if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn)}else{effectFn()}})
}

我们值执行副作用函数的时候,先判断以下是否存在调度器,如果存在就把副作用函数放到调度器中执行,由用户自己控制如何执行。
这样实现要求 我们只要在调用effect方法的时候,传入一个调度器就好了,如以下:

effect(()=>{console.log(obj.foo)
},{scheduler(fn){setTimeout(fn)}
})

这样可以控制副作用函数执行的时机,我们也要控制副作用函数的次数,如以下代码:

const data = { foo: 1}
const obj = new Proxy(data,{/*...*/})
effect(()=>{console.log(obj.foo)
})
obj.foo++
obj.foo++

通过运行以上代码,我们发现会打印

1
2
3

2只是过渡状态,我们只关注结果,而不关注过程,在这里,我们调度器可以很简单的实现:

//定义一个任务队列
const jobQueue = new Set()
//使用promise.resolve()创建一个promise实例,我们用它将一个任务放入微任务执行
const p = promise.resolve()
//给一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob(){if(isFlushing) returnisFlushing = truep.then(()=>{jobQueue.forEach(job => job())}).finally(()=>{isFlushing = false})
}
effect(()=>{console.log(obj.foo)
},{scheduler(fn){jobQueue.add(fn)flushJob()}
})

我们可以看到 我们把副作用函数的执行放到了微任务执行,使用set集合是为了set集合的去重能力,只有当副作用函数的调度器执行完成后,才会进行微任务的执行,这样就把重复的副作用函数过滤掉了

小结

到这里我们就可以发现,我们以及写出了相对完善的响应式系统了,下一章,我们学习一下,vue的计算属性和lazy


http://www.ppmy.cn/ops/102511.html

相关文章

自然语言处理系列四十五》Elasticsearch搜索引擎》Elasticsearch入门及技术原理

注&#xff1a;此文章内容均节选自充电了么创始人&#xff0c;CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》&#xff08;人工智能科学与技术丛书&#xff09;【陈敬雷编著】【清华大学出版社】 文章目录 自然语言处理系列四十五Elasticsearch搜索引擎》Elasticsearch入…

Mysql5.7.40安装步骤

mysql5.7.40数据库部署安装_数据库_weixin_44568463-亚马逊云科技技术品牌专区

使用 ASP.NET Core 与 Entity Framework Core 进行数据库操作

使用 ASP.NET Core 与 Entity Framework Core 进行数据库操作 Entity Framework Core&#xff08;EF Core&#xff09;是ASP.NET Core中的一个轻量级ORM框架&#xff0c;提供了以面向对象的方式与数据库进行交互的能力。本文将通过Visual Studio 2022详细介绍如何使用EF Core进…

在Android中的widge组件是什么?

目录 Widget 的特点 创建 Android Widget 的步骤 Widget 的主要功能 常见的 Widget 类型 总结 在 Android 中&#xff0c;Widget&#xff08;小部件&#xff09; 是一种特殊的 UI 组件&#xff0c;通常称为 "App Widget"。它是小型的、可以放置在设备主屏幕上的…

【区块链 + 司法存证】印记区块链电子印章 | FISCO BCOS应用案例

电子印章作为传统物理印章的数字化锚定&#xff0c;除了拥有和物理印章一样的法律效力外&#xff0c;还能够有效地为企业增效降 本提质。近年来&#xff0c;随着国家双碳目标的提出以及全球新冠疫情&#xff0c;进一步加速了企业数字化转型的步伐&#xff0c;电子印章 的价值也…

批量在多台Linux机器上安装OpenJDK

上一次我们实践了手动安装OpenJDK的过程&#xff0c;并且完成了用脚本一键安装的试验。但是本质上&#xff0c;我还是每台机器上单独进行操作。那这就产生了一个问题&#xff0c;如果我需要一次性在多台机器上部署安装&#xff0c;需要怎么操作呢。 问题分析 假设我的目的是在…

扁线电机介绍

相比于圆线&#xff0c;扁线因为扁平矩形的特殊性能够让线圈缠绕更加紧密&#xff0c;槽满率由原先的40%提升到70%。 这意味着相同体积下线圈中的导线更多&#xff0c;电流的传导效率更高&#xff0c;能够减少电阻损耗&#xff0c;产生的磁场更强&#xff0c;电机功率也就更大&…

IP地址与SSL证书:保障网络安全的关键

在数字时代&#xff0c;网络安全至关重要&#xff0c;而SSL&#xff08;安全套接层&#xff09;证书作为加密用户与服务器之间数据传输的利器&#xff0c;扮演着不可或缺的角色。然而&#xff0c;谈及SSL证书时&#xff0c;一个常见的误区是它们通常与域名绑定&#xff0c;而非…

【前端】理解与使用sessionStorage、localStorage与cookie

深入理解与高效使用 sessionStorage、localStorage 与 cookie 背景 在构建一个多页面的 Vue web 应用时&#xff0c;我面临了一个关键问题&#xff1a;如何有效地管理用户的登录状态。为了减少对服务器的不必要请求&#xff0c;我尝试了全局状态注入的方法&#xff0c;但这种…

【通俗理解】深度学习特征提取——Attention机制的数学原理与应用

【通俗理解】深度学习特征提取——Attention机制的数学原理与应用 关键词提炼 #深度学习 #特征提取 #Attention机制 #CNN #Transformer #关联特征 #MLP #拟合处理 第一节&#xff1a;Attention机制的类比与核心概念 1.1 Attention机制的类比 Attention机制可以被视为一个“特…

【kafa系列】kafka如何保证消息不丢失

【kafa系列】kafka如何保证消息不丢失 Apache Kafka通过多种机制来确保消息不丢失&#xff0c;这些机制包括但不限于副本机制、ISR&#xff08;In-Sync Replicas&#xff09;机制、ACK&#xff08;Acknowledgment&#xff09;机制、幂等生产者&#xff08;Idempotent Producer&…

K8S对接Ceph分布式存储

文章目录 一、Ceph理论知识1、Ceph简介2、Ceph分布式存储的优点3、Ceph核心组件 二、部署Ceph高可用集群1、服务器环境信息2、部署前环境准备工作3、部署Ceph监控服务Monitor4、激活Ceph存储服务OSD 三、K8S对接Ceph存储1、K8S对接Ceph RBD实现数据持久化2、基于Ceph RBD生成PV…

计算机视觉编程 1(图片处理)

目录 灰色度 缩略图 拷贝粘贴区域 调整图像尺寸 旋转图像45 画图线、描点 灰色度 灰度是指图像中每个像素的亮度值&#xff0c;用来描述图像中各个像素的明暗程度。在计算机视觉中&#xff0c;灰度可以通过以下方式来计算&#xff1a; 1. 平均值法&#xff1a;将图像中每…

计算机毕业设计推荐-基于python的个性化旅游路线推荐平台

&#x1f496;&#x1f525;作者主页&#xff1a;毕设木哥 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; 实战项目 文章目录 实战项目 一、基于python的个性化旅游路线…

ubuntu20 安装ros noetic版本

【ROS】Ubuntu20.04卸载重装ROS_ubuntu20.04卸载ros-CSDN博客 错误处理——rosdep init&#xff0c;rosdep update失败解决方案_rosdep init出错-CSDN博客 ubuntu 20.04解决在处理时有错误发生&#xff1a; /var/cache/apt/archives/python3-catkin-pkg-modules_0.4.24-1_all…

NoSQL数据库-Redis集群详解及案例实现

一、 关系型数据库和 NoSQL 数据库 1.1 数据库主要分为两大类&#xff1a;关系型数据库与 NoSQL 数据库 关系型数据库&#xff0c;是建立在关系模型基础上的数据库&#xff0c;其借助于集合代数等数学概念和方法来处理数据库中的数据主流的 MySQL、Oracle、MS SQL Server 和 D…

基于JAVA的专利资源共享平台

项目介绍 基于JAVA的专利资源共享平台系统是一个集专利信息展示、资源共享、交易服务等功能于一体的综合性平台。该系统利用JAVA语言的强大功能和广泛的生态系统&#xff0c;结合数据库技术、Web开发技术等&#xff0c;为用户提供了一个高效、安全、便捷的专利资源共享和交易环…

【C++】日期和时间

C 提供了多种处理日期和时间的功能&#xff0c;主要通过标准库 <ctime> 和 <chrono> 提供。以下是 C 中处理日期和时间的功能介绍及其用法&#xff1a; 1. <ctime> 库 <ctime> 是 C 中处理时间的传统库&#xff0c;提供了一些基本的时间操作函数。这…

Amazon Bedrock 实践:零基础创建贪吃蛇游戏

本文探讨了如何利用 Amazon Bedrock 和大型语言模型&#xff0c;快速创建经典的贪吃蛇游戏原型代码。重点展示了利用提示工程&#xff0c;将创新想法高效转化为可运行代码方面的过程。文章还介绍了评估和优化提示词质量的最佳实践。 亚马逊云科技开发者社区为开发者们提供全球的…

SD三分钟入门!秋叶大佬24年8月最新的Stable Diffusion整合包V4.9.7来了~

先感谢赛博菩萨秋葉大佬&#xff0c;开发绘世启动器&#xff0c;**8月15日更新了秋叶整合包最新版本4.9。**文末有安装包&#xff01;&#xff01; SD整合包可以扫描下方,免费获取 1 什么是 Stable Diffusion&#xff1f; Stable Diffusion&#xff08;简称SD&#xff09;是…