【vue设计与实现】响应系统的作用与实现 4-避免无限递归循环&调度执行

《Vuejs 设计与实现》第 4 章(响应式系统)( 下 ) 本章我们深入探讨了副作用函数与响应式数据的关系。我们明确了响应式数据的基本实现依赖于“读取”和“设置”操作的拦截,连接副作用函数与响应式数据。当读取数据,我们将副作用函数存储,而在设置数据时,我们将存储的副作用函数取出并执行,这是响应系统的核心实现原理。我们构建了一个完整的响应系统,用 WeakMap 和 Map 创建新的存储结构,确保响应式数据与副作用函数之间的精确关联。此外,我们分析了 WeakMap 和 Map 的差异,其中 WeakMap 的弱引用特性不会妨碍垃圾回收。 阅读详情

避免无限递归循环

看下面的例子:

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

effect(()=>obj.foo++)

可以看到这里有个自增操作obj.foo++,该操作会引起栈溢出:
Uncaught RangeError: Maximum call stack size exceded

下面来搞清楚这个问题是为什么。
实际上,把obj.foo++这个操作分开来看,相当于:

effect(()=>{
	obj.foo = obj.foo + 1
})

这里机会读取obj.foo的值,又会设置obj.foo的值,这就是导致这个问题的原因。这里代码的执行流程如下:
首先读取obj.foo的值,触发track操作,将当前副作用函数收集到“桶”中;
接着加1赋值给obj.foo,此时会触发trigger操作,即把“桶”中的副作用函数取出并执行。
但问题是该副作用函数正在执行,还没有执行完,就要开始下一次的执行。这样就会导致无限递归地调用自己,于是就产生了栈溢出。

这里可以观察发现,读取和设置操作是在同一个副作用函数内进行的。此时track收集的和trigger要触发的都是activeEffect。要解决这个问题,我们可以在trigger动作发生时增加守卫条件。
如果trigger所执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。代码如下:

function trigger(target, key){
	const depsMap = bucket.get(target)
	if(!depsMap) return
	const effects = depsMap.get(key)

	const effectsTorRun = new Set()
	effeects && effects.forEach(effecetFn => {
		// 如果trigger触发执行的副作用与当前正在执行的副作用函数相同,则不触发执行
		if (effectFn != activeEffect){
			effectsToRun.add(effectFn)
		}
	})
	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

调整代码执行顺序可以很简单的解决这个问题,但是有没有办法在不调整代码执行顺序就实现需求的。这时就需要响应系统支持调度。

可以为effect函数设计一个选项参数options,允许用户指定调度器,如下:

effect{
	() => {
		console.log(obj.foo)
	},
	// options
	{
		// 调度器 scheduler是一个函数
		scheduler(fn){
			// ...
		}
	}
}

用户在调用effect时,可以传递第二个参数options,options是一个对象,其中允许指定scheduler调度函数;
同时在effect函数内部需要把options选项挂载到对应的副作用函数上。

function effect(fn, options = {}){
	const effectFn = () => {
		cleanup(effectFn)
		activeEffect = effectFn
		effectStack.push(effectFn)
		fn()
		effectStack.pop()
		activeEffect = effectStack[effectStack.length - 1]
	}
	// 将 options 挂载到 effectFn上
	effectFn.options = options
	effectFn.deps = []
	// 执行副作用函数
	effectFn()
	
}

有了调度函数,就可以在trigger函数中触发副作用函数重新执行时,直接调用用户出传递的调度器函数,从而把控制权交给用户:

function trigger(target, key){
	const depsMap = bucket.get(target)
	if(!depsMap) return
	const effects = depsMap.get(key)

	const effectsToRun = new Set()
	effeects && effects.forEach(effecetFn => {
		// 如果trigger触发执行的副作用与当前正在执行的副作用函数相同,则不触发执行
		if (effectFn != activeEffect){
			effectsToRun.add(effectFn)
		}
	})
	effectsToRun.forEach(effectFn => {
		// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
		if(effectFn.options.scheduler){
			effectFn.options.scheduler(effectFn)
		}else{
			// 否则直接执行副作用函数(之前的默认行为)
			effectFn()
		}
	})
}

注意要把当前副作用函数作为参数传递到调度器中

有了这些,就可以实现前文的需求了,如下代码:

const data = {foo:1}
const obj = new Proxy(data,{/*...*/})
effect{
	() => {
		console.log(obj.foo)
	},
	// options
	{
		// 调度器 scheduler是一个函数
		scheduler(fn){
			// 将副作用函数放到宏任务队列中执行
			setTimeout(fn)
		}
	}
}
effect(()=>{
	console.log(obj.foo)
})
obj.foo++
console.log('结束了')

这里使用setTimeout开启一个宏任务执行副作用函数fn,这样就能改变代码执行顺序。

除了控制副作用函数的执行顺序,通过调度器还可以做到控制它的执行次数,这一点也尤为重要。看下面例子:

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

effect(()=>{
	console.log(obj.foo)
})

obj.foo++
obj.foo++

在没有指定调度器的情况下,输出如下:
1
2
3

如果我们只关心最终结果而不关心过程,那么执行三次打印操作是多余的,我们期望的打印结果是:
1
3

基于调度器我们可以很容易地实现此功能

// 定义一个任务队列
const jobQueue = new Set()
// 使用Promise.resolve()创建一个promise实例,我们用它将一个任务添加到任务队列
const p = Promise.resolve()

// 一个标志代表是否在刷新队列
let isFlushing = false
function flushJob(){
	// 如果队列在刷新就什么都不做
	if(isFlushing) return
	// 设置为true,代表正在刷新
	isFlushing = true
	// 在微任务队列中刷新jobQueue队列
	p.then(()=>{
		jobQueue.forEach(job=>job())
	}).finally(()=>{
		//结束后重置 isFlushing
		isFlushing = false
	})
}

effect(()=>{
	console.log(obj.foo)
}, {
	scheduler(fn){
		//每次调度时,将副作用函数添加到jobQueue队列中
		jobQueue.add(fn)
		// 调用flushJob刷新队列
		flushJob()
	}
})

obj.foo++
obj.foo++

整段代码的效果是,连续对obj.foo执行两次自增操作,会同步且连续执行两次scheduler调度函数,也就是同一个副作用函数会被jobQueue.add(fn)添加两次,但由于Set数据结构的去重能力,最终jobQueue中只会有一项,就是当前的副作用函数。
类似地,flushJob也会同步且连续执行两次,但由于isFlushing,实际上flushJob函数在一个事件循环内只会执行一次,即在微任务内执行一次。
当微任务队列开始执行时,就会遍历jobQueue并执行里面存储的副作用函数,由于此时jobQueue队列内只有一个副作用函数,所以只会执行一次,并且当它执行时,字段obj.foo的值已经是3了,这样就实现期望的功能了。
实际上Vue.js内部实现了一个更加完善的调度器,思路和上文的相同。

知识扩展:
Promise的resolve()方法可直接返回Promise成功对象,把一个普通对象转为Promise对象

resolve()实现

// 类方法Promise.resolve
// 普通的值
const promise = Promise.resolve({ name: "swh" })
// 相当于
// const promise2 = new Promise((resolve, reject) => {
//   resolve({ name: "swh" })
// })
 
promise.then(res => {
  console.log("res:", res)    //res: { name: 'why' }
})

更多内容参考:https://es6.ruanyifeng.com/#docs/promise#Promise-resolve

用Spirent Test Center测试交换机Head of Line(HOL)Blocking 测试目的:设备执行拥塞控制,使非阻塞端口不受阻塞端口影响 预设条件:在RFC2889里按以下步骤设置,端口1和端口3同时向端口4发送数据包,端口1向端口2发送数据包 测试步骤:运行设置好的RFC2889 测试结果:所有frame size都显示没有Head of Line(HOL),即非阻塞端口不受阻塞端口所影响 ... 阅读详情

相关推荐

C# : 计算并输出这一天是这一年的第几天

C#之输入某年,然后输入某月,再输入某日,计算并输出这一天是这一年的第几天的功能算法

saitoDeng的博客 970

System.StackOverflowException

今天按着书上的例子写呀写,写了一下午终于做出了一个三层模式的通讯录(当然很简单),但是,弄了最后却碰到个运行时的异常,弄得我这个asp.net菜鸟郁闷了再郁闷.异常如下:发生类型为 System.StackOverflowException 的异常。看了网上的资料说是,由于递归调用了自己.编译能够通过,但是运行会产生异常.msdn上说:关于异常的疑难解答:System.StackOverf

------------------ 1万+

基于Gardner环的QPSK定时同步MATLAB仿真:四倍采样与多场景性能分析

本文详细介绍了基于Gardner环的QPSK定时同步MATLAB仿真方法,重点分析了四倍采样技术在定时同步中的应用与性能优化。通过MATLAB代码实例展示了Gardner环的实现原理、多场景性能分析及常见问题解决方案,为数字通信系统的定时同步设计提供实用参考。

weixin_29083475的博客 373

写属性时遇到无限递归或无限循环

        最近在写学生选课系统,我看同学的是把学生写成一个类,我觉得这样比较好,就想把自己的也写成一个类。没想到一开始就出了问题。看下边: public class Student { string 学号; public string 学号 {

栩风 6035

MiniMax H3 + ComfyUI 工作流搭建指南:从整合包到视频生成

AI视频生成正成为内容创作的新基础设施,而ComfyUI作为可视化工作流引擎,将复杂的模型推理过程拆解为节点与连线,让本地视频生成变得可复用、可批量。理解这一原理后,用户无需编写代码,只需掌握模型加载、提示词控制、采样与解码的基本链路,即可搭建出稳定的视频生产管线。MiniMax H3作为前沿视频生成模型,在ComfyUI中通过权重文件和自定义节点落地,配合整合包可大幅降低环境配置门槛。从硬件评估、目录结构到节点连接与参数调优,本文系统性梳理了从零跑通一条视频生成工作流的完整路径,帮助用户在批量素材生成、风

weixin_29057065的博客 414

Java开发者必看:5分钟搞定PaddleOCR表格识别,导出Excel和HTML超详细教程

本文为Java开发者提供了集成PaddleOCR实现智能表格识别的详细教程,包括环境配置、核心代码实现、结果导出到Excel和HTML的步骤,以及性能优化技巧。通过PaddleOCR技术,开发者可以快速将图片表格转换为结构化数据,大幅提升工作效率。

weixin_30872499的博客 98

“System.StackOverflowException”类型的未经处理的异常出现在 mscorlib.dll 中。

<br /><br /> <br />疑难解答提示:<br />确保您没有无限循环或无限递归。

拓 7015

计算机组成原理课程设计_微程序控制的简单模型机设计

微程序控制的简单模型机设计 要求 基本要求 完成一个简单主机的设计,模型机应具备如下能力:在自行设计的指令集基础上,运行自行编写的简单用户程序,实现特定运算功能。 弹性要求 在此基础上,完成必要的扩展,实现模型机的功能扩展设计,包括:实现多种运算功能(指令集的扩充)、支持更为丰富的寻址方式、有更为完善的微程序设计等。 实验软件和硬件环境 QuartusII, FPGA计算机组...

sailist的记录站 3万+

关于 Java 异常机制的补充

为什么 Java 中打开物理资源,如磁盘文件、网络连接、数据库连接等,必须需要显式的关闭? JVM 提供的垃圾回收机制只负责堆内存分配出来的内存,打开的物理资源,GC 是不进行回收的,所以需要手动的关闭。 如何正确的关闭资源? 在 finall 中执行资源关闭操作。 保证关闭资源前资源不为 null,因为存在资源初始化前就发生异常情况,所以在 finally 语句中对资源对象进行非空判断。...

Strange_Monkey的博客 325

Windows/Mac/Linux三系统保姆级教程:用llama.cpp将safetensors模型转gguf格式(附常见错误解决)

本文提供Windows、Mac和Linux系统下使用llama.cpp将safetensors模型转换为gguf格式的详细教程,涵盖环境配置、转换原理及常见错误解决方案。通过跨平台兼容性处理,帮助开发者高效完成模型格式转换,优化边缘计算性能。

weixin_29227425的博客 270

6、基因组数据可视化与甲基化分析:从基础操作到深入洞察

本文详细介绍了基因组数据可视化和全基因组DNA甲基化分析的方法和技术,包括基因组查看器(IGV)的基本操作、单核苷酸变异的可视化、DNA甲基化与CpG岛的关联分析、统计建模与显著甲基化区域识别,以及不同细胞系之间的甲基化模式差异分析。通过具体案例展示了如何利用MATLAB进行数据分析和可视化,为基因组研究、疾病机制探索及个性化医疗提供了技术支撑和实践参考。

qsc901234的博客 295

【Vue.js设计与实现】第二篇第4章:响应系统-响应系统的作用与实现-阅读笔记

从高层设计的角度去探讨框架需要关注的问题。第二篇 响应系统。

karshey的博客 1673

梯度提升决策树GBDT原理与代码实例讲解

梯度提升决策树GBDT原理与代码实例讲解 1.背景介绍 1.1 机器学习发展历程 机器学习是人工智能领域的一个重要分支,旨在使计算机具备学习和改进自身性能的能力。随着大数据时代的到来,海量的数据为机器学习算法提供了广阔的应用空间。在

AI天才研究院 1086

《Vue.js设计与实现》学习笔记_第四章 响应系统的作用与实现

首先介绍了副作用函数和响应式数据的概念,以及它们之间的关系。响应系统的根本实现原理:一个响应式数据最基本的实现依赖于对“读取“和“设量“操作的拦截,从而在副作用函数与响应式数据之同建立联系。当“读取“操作发生时,我们将当前执行的刷作用函数存储到“桶”中;当“设置”操作发生时,再将副作用函数从“桶“里取出并执行。实现一个相对完善的响应系统:使用 Meakap 配合MaP 构建了新的“桶”结构,从而能够在响应式数据与副作用函数之间建立更加糖确的联系。

weixin_61586448的博客 784

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

《Vue.js 技术与实现》第4章 响应式系统的作用和实现 阅读总结

☁️ 2022.03.20 15:30 这一章题序中上来就抛出了如下问题: 什么是响应式数据和副作用函数? 实现中如何避免无限递归? 为什么需要嵌套的副作用函数? 两个副作用函数之间会产生哪些影响? 花费一个晚上+一个下午读完了这一章,读完的第一感觉是信息量巨大。即使之前自己实现过一遍属于自己的 mini-vue,看完本章依然觉得填补了我很多知识盲区,收获很多,但一时很难消化。 此刻难消化的原因:很难在头脑中重现源码中针对哪些问题给出了哪些解决方案? 对于看完之后合上书本让人感觉虚空的问题,决定用自己方

兰亭古墨 1649

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an &ldquo;advanced algorithms&rdquo; book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

Vue3响应式系统(三)

当obj.foo执行两次自增操作,会同步且连续执行两次scheduler调度函数,这意味着同一个副作用函数会被jobQueue.add(fn)语句添加两次,由于Set去重能力,最终jobQueue中只会有一项,即当前副作用函数。obj.foo++ ====> obj.foo = obj.foo + 1 ,首先会读取obj.foo的值,这就会触发get函数中的track收集函数,之后又会将obj.foo的值赋值给obj.foo,这时又会触发trigger触发函数。此时,便不会无限循环,而是只执行一次。

00后的程序员 1149

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

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

effect函数执行的时候 首先会把effectFn1函数放进来,activeEffect函数执行的就是effectFn1函数,到这里时没有问题,但是在fn执行的时候,我们就把effectFn2函数赋值给了activeEffect函数,这时候我们做了get操作,获取了obj.foo,此事,对应的activeEffect函数对应的effectFn2函数,所以foo这个key值对应的副作用函数就变成了effectFn2而不是effectFn1了。我们执行上面代码,我们会发现,会一直无限循环,这是为什么呢?

tinglis的博客 1263

《Vue.js设计与实现》学习笔记 | 响应系统的作用与实现

本章从 Vue.js 的响应式数据系统入手,探讨其核心机制。先是介绍了副作用函数及其原理,通过对数据的读取和赋值捕获,实现响应式。接着讲解了利用 `Proxy` 和 `WeakMap` 实现数据与副作用函数的绑定和触发。为解决副作用函数重复执行的问题,引入了依赖清理机制,并通过封装 `track` 和 `trigger` 函数优化代码。针对嵌套 `effect`,采用栈结构管理活动副作用函数,避免误收集。最后通过调度执行机制,实现了副作用函数的可调度性,从而构建了一个完善的响应式系统。

still-soda的博客 1368

如何一步步实现一个相对完整的响应式系统?(上)

本章目标:1. 讨论什么是响应式数据和副作用函数,实现一个相对完善的响应式系统?2. 如何避免无限递归?3. 为什么需要嵌套副作用函数?4. 两个副作用函数之间会产生哪些影响?5. ......

繁华落尽 1179

Vue3响应式系统实现原理(二)

本文根据VueJs核心团队成员霍春阳《Vue.js设计与实现》第四章整理,推荐直接购买正版书籍系统学习。 本文主要内容: (1)分支切换与cleanup (2)嵌套的effect与effect栈 (3)避免无限递归循环 (4)调度执行...

CherishTheYouth的博客 515

Vue.js设计与实现读书笔记二 第四章 响应系统的作用与实现

响应式数据是当对象数据(设置)改变的时候,会执行读取数据的操作,

qq_24986271的博客 433

读《Vue.js设计与实现》- 第四章(中)

此文章为了记录自己对读这本书的感悟和对书中所提的内容的个人理解,文章将分为多次发布,流程是先仔细阅读其中的每一章内容,然后再根据自己的理解手写代码,最后结合文章内容和个人理解形成该文章,以便加深印象和提供给有需要的人。

god_hands1的博客 942

《vue.js的设计与实现》4-6章小结

一、响应系统的整体实现 如p49页的代码所示,Vue3使用es6中的proxy进行数据劫持,当副作用函数(用到某个属性的方法)第一次执行的时候,会触发拦截器中的get方法,然后调用了track方法将这个副作用函数添加到存储副作用函数的桶中,相当于收集依赖的过程。 这个存储副作用函数的桶的结构为:用weakmap存储不同对象(weakmap当key没有引用被垃圾回收器回收了,对应的键和值就访问不到了,防止内存溢出),用map存储对象的各个属性,各属性的副作用函数用set来存储(可以去重)。(这样的设计当一个属

weixin_43206959的博客 170

实现vue3响应式系统核心-watch

今天我们来看看watch的实现。watch本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。实际上,watch的实现本质上就是利用了effect以及选项。代码地址: https://github.com/SuYxh/share-vue3代码并没有按照源码的方式去进行组织,目的是学习、实现 vue3 响应式系统的核心,用最少的代码去实现最核心的能力,减少我们的学习负担,并且所有的流程都会有配套的图片,图文 + 代码,让我们学习更加轻松、快乐。每一个功能都会提交一个commit。

前端那些事@时光 1378

【vue设计与实现】响应系统的作用与实现 6-watch的实现原理

watch值的就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数,例如: watch(obj, () =&gt; { console.log('数据变了') }) // 修改响应数据的值,会导致回调函数执行 obj.foo++ 假设obj是一个响应数据,使用watch函数观测它,并传递一个回调函数,当修改响应式数据的值时,会触发该回调函数执行。 watch的实现本质上就是利用effect以及options.scheduler选项,如下代码所示: effect(()=&gt;{ conso

loyd3的博客 428

Vue3响应式原理与Proxy实现详解

响应式编程是现代前端框架的核心机制,它通过自动追踪数据依赖关系实现UI与状态的同步更新。其原理基于ES6 Proxy特性,能够拦截对象操作实现细粒度的依赖收集与更新触发。相比传统的Object.defineProperty方案,Proxy提供了更全面的属性拦截能力,包括动态属性增删和数组变化检测。在Vue3框架中,这种技术显著提升了性能表现,特别是在处理大型对象和复杂数据结构时。通过TypeScript实现的简易响应式库,开发者可以深入理解reactive、ref、effect等核心概念的工作机制,这些知识

364
上一篇: 【vue设计与实现】响应系统的作用与实现 3-嵌套effect和effect栈
下一篇: 【vue设计与实现】响应系统的作用与实现 5-计算属性computed与lazy
loyd3
博客等级 码龄15年 612粉丝 281原创
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值