深入理解Vue3.js响应式系统设计之栈结构和循环问题

如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~

作者:前端小王hs

阿里云社区博客专家/清华大学出版社签约作者/CSDN百万访问前端博主/B站千粉前端up主

此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来

书籍:《Vue.js设计与实现》 作者:霍春阳

本篇博文将在书第4.5节至4.7节的基础上进一步解析,附加了测试的代码运行示例,以及对书籍中提到的ES6中的数据结构及其特点进行阐述,方便正在学习Vue3想分析Vue3源码的朋友快速阅读

如有帮助,不胜荣幸

在上一节深入理解Vue3.js响应式系统基础逻辑中,我们阐述了Vue3.js通过ES6ProxySetWeakMapMap等语法和数据结构实现了基础的响应式系统,那么在这节笔记中,笔者将会对书中第4.5节至4.6节的内容进行阐述

需要注意的是,第4.5节至4.6节的内容主要讲解了effect嵌套及问题和解决方案、避免effect无限递归两个方面,内容相对于4.1节至4.5节少许多,但却是实现computedwatch计算属性的不可缺少的步骤

所以这节笔记会相对较少字数

嵌套的effect和effect栈

4.5节的开篇中,作者提到了Vue.js的渲染函数就是在effect中执行的,我们知道,effect就是副作用函数,同时,Vue.js的组件也是通过渲染函数去return的,代码如下:

const Foo = {render() {return /* ... */;}
};

而组件是可以嵌套的,进而引出了嵌套effect,并进一步引出在嵌套情况下effect函数将会面临的问题,其实是指我们在上一节设计的响应式系统不完善的问题

我们知道,副作用函数会和属性进行关联,关系如下:

target └── key└── effectFn

具体的实现过程分为两步,首先是执行并注册副作用函数(复习一下上节内容),代码如下:

// 用一个全局变量存储被注册的副作用函数
let activeEffect;function effect(fn) {const effectFn = () => {// 调用 cleanup 函数完成清除工作cleanup(effectFn); // 新增activeEffect = effectFn;fn();};effectFn.deps = [];effectFn();
}function cleanup(effectFn) {//遍历effectFn.deps...//重置effectFn.deps...
}

第二步是在Proxy.get()中将副作用函数会和属性进行关联

// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {// 没有 activeEffect,直接 returnif (!activeEffect) return;let depsMap = bucket.get(target);if (!depsMap) {bucket.set(target, (depsMap = new Map()));}let deps = depsMap.get(key);if (!deps) {depsMap.set(key, (deps = new Set()));}// 全局变量activeEffectdeps.add(activeEffect);
}

嵌套带来的问题

那么,假设我们现在有一个effect嵌套effect的函数,代码如下:

effect(() => {Foo.render();// 嵌套effect(() => {Bar.render();});
});

我们来分析一下这个嵌套effect()执行的过程:

  1. 匿名函数()=>{...}被传入effect()中并执行effectFn(),我们给这个effectFn()起个别名effectFn1()
  2. 由于不是修改操作,我们可以忽略clean()effectFn1()activeEffect拥有共同地址,执行传入的Foo.render(),后续流程忽略
  3. 执行effect(() => {Bar.render();});
  4. 在第3步中,重复了第2步的操作,但此时的effectFn2()不等于第二步中的effectFn1(),为啥?两个effectFn()内的fn()不一样!
  5. 而此时activeEffect的地址被替换(覆盖)成effectFn2(),然后执行Bar.render(),至此结束

所以我们可以得到一个结论,activeEffect总是与内层嵌套的副作用函数地址相同

现在我们再来看书中给出的代码例子:

// 原始数据
const data = { foo: true, bar: true };
// 代理对象
const obj = new Proxy(data, { /* ... */ });// 全局变量
let temp1, temp2;// effectFn1 嵌套了 effectFn2
effect(function effectFn1() {console.log('effectFn1 执行');effect(function effectFn2() {console.log('effectFn2 执行');// 在 effectFn2 中读取 obj.bar 属性temp2 = obj.bar;});// 在 effectFn1 中读取 obj.foo 属性temp1 = obj.foo;
});

通过去上面嵌套effect的分析,我们知道activeEffect总是与内层嵌套的副作用函数地址相同,在书中代码的例子中,activeEffect等于effectFn2()内的effectFn(),而这个effectFn()内部执行的是什么?是function effectFn2()

代码执行到temp1 = obj.foo;时,就会触发obj.foo的读取,而此时的全局变量activeEffect绑定的逻辑是执行effectFn2()

所以,当我们修改obj.foo时,与我们想要的触发执行effectFn1()结果不一致,因为执行的是effectFn2()

这就是目前代码下的嵌套effect导致的问题——activeEffect总是指向内层嵌套的副作用函数地址,也就是同一时刻activeEffect所存储的副作用函数只能有一个(原文)

解决方案

Vue.js团队是如何处理的呢?答案是结构,而在JavaScript中,通常使用数组实现

其实就是一种先进后出的思维,比方说在上面关于Foo组件嵌套Bar组件执行的过程,我们知道在第3步的时候activeEffect被覆盖了,那我们是不是可以设计一个数组effectStack,在第2步的时候把此时的activeEffecteffectFn1()存进effectStack,然后当执行第3步的时候,把effectFn2()赋值给activeEffect,然后再把这个effectFn2()存进进effectStack,那么此时的effectStack结构如下:

effectStack = [effectFn1(),effectFn2()]

副作用函数赋值给activeEffect的下一步是执行Fn(),在执行Fn()的过程中会将此时的activeEffect与对应的target.key相关联

别忘了我们的目的是什么,我们只是想target.key关联的是与之对应的effectFn

ok!关联之后,我们再把effectStack的最后一个(此时为effectFn2())删除,然后再把activeEffect变为effectStack里的effectStack.length - 1项,其实也就是当前的最后一项,在上述例子中就是effectFn1()

那么再回看关于最后一句代码是读取obj.foo的例子,显而易见,使用了之后,obj.foo关联的就是effectFn1()

最后,我们来看下具体的实现代码,代码如下:

let activeEffect;
const effectStack = [];function effect(fn) {const effectFn = () => {cleanup(effectFn);activeEffect = effectFn;effectStack.push(effectFn);fn();effectStack.pop();activeEffect = effectStack[effectStack.length - 1] || null;};effectFn.deps = [];effectFn();
}

关键的几行代码如下:

  1. activeEffect = effectFn,把当前的effectFn赋值给activeEffect
  2. effectStack.push(effectFn),把effectFn推入
  3. 执行fn(),在这一步会在Proxy.get()中形成与key的关联
  4. effectStack.pop(),把栈顶effect弹出
  5. activeEffect = effectStack[effectStack.length - 1] || null等于当前栈顶函数

小记:思路非常的清晰,我在看这本书的时候,感觉读这本书不仅可以学习Vue.js的底层实现逻辑,提高自己的代码理解水平,还能学习在面对复杂情况下的解决方案,只能说真是一本好书

下面,我们接着跟着书的章节,进入4.6节,去解决无限循环的问题

无限循环

这一节的问题来自下面这段代码:

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

在前面的设计过程中。我们知道总体的流程是读取的时候存储副作用函数修改的时候再把副作用函数拿出来

但现在我们来分析一下obj.foo++这段副作用函数的执行流程

  1. obj.foo触发了读取操作,将执行() => obj.foo++effectFn存入activeEffect
  2. obj.foo++触发了修改操作,将() => obj.foo++(外层是effectFn)从里拿出来执行
  3. obj.foo触发了读取操作,将执行() => obj.foo++effectFn存入activeEffect
  4. …开始循环

其实按正常的逻辑来说,应该等读取修改完成后,再进行其他的逻辑,但现在修改都还没完成,又进入读取操作了

这就是问题所在,既会读取 obj.foo 的值,又会设置 obj.foo 的值,而这就是导致问题的根本原因(原文)

这里可以看出要设计一个基本的响应式系统,要考虑的情况是十分多的,要解决的问题也是十分多的

解决无限循环

从上面的执行流程可以看出,出现问题的根本原因在于读取和设置操作是在同一个副作用函数内进行的(原文),当前的activeEffect和从里拿出来的effectFn的一样的,所以就可以在trigger里进行判断,如果发现保存在全局变量的activeEffect和从里拿出来的是一样的,那么就不执行,代码如下:

function trigger(target, key) {const depsMap = bucket.get(target)if (!depsMap) returnconst effects = depsMap.get(key)const effectsToRun = new Set()effects && effects.forEach(effectFn => {// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行if (effectFn !== activeEffect) { // 新增effectsToRun.add(effectFn)}})effectsToRun.forEach(effectFn => effectFn())// effects && effects.forEach(effectFn => effectFn()) // 原来的代码
}

那么到这里,我们就跟着书籍解决了嵌套自增逻辑带来的问题,下一节笔记我们来探讨如何实现调度执行,这是实现响应式核心computed()的关键内容

谢谢大家的阅读,如有错误的地方请私信笔者

笔者会在近期整理后续章节的笔记发布至博客中,希望大家能多多关注前端小王hs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/26397.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

黄金投资软件选择标准:多角度全面考量指南

随着金融科技的迅猛发展,越来越多的投资者倾向于通过线上平台进行黄金投资。然而,面对市场上琳琅满目的黄金投资软件,如何选择一款既安全可靠又功能齐全的软件,成为了投资者们普遍关注的问题。 黄金投资软件的选择不仅直接影响到…

Excel自定义排序和求和

概览 excel作为办公的常备工具,好记性不如烂笔头,在此梳理记录下,此篇文章主要是记录excel的自定义排序和求和 一. 自定义排序 举个例子 1. 填充自定义排序选项 实现步骤: 选定目标排序值;文件->选项->自定…

CSS Display(显示)

CSS Display(显示) 概述 CSS(层叠样式表)中的display属性是控制元素如何显示的关键属性。它决定了元素的盒模型类型,即元素是块级元素、内联元素还是其他类型的元素。display属性对于网页布局和元素样式的控制至关重要。 基本用法 块级元…

取模软件测试版生成的有斜线,但测试字库的功能是好用无限制。只需要自己开发一个字库生成软件。IDA工具,如何搜中文

IDA工具,如何搜中文 在 IDA (Interactive Disassembler) 中搜索中文字符串,可以按照以下步骤操作: 方法一:使用“搜索文本”功能 打开 IDA 并加载目标文件:启动 IDA 并打开你需要分析的二进制文件。打开搜索文本对话…

Allegro光绘Gerber文件、IPC网表、坐标文件、装配PDF文件导出打包

Allegro光绘Gerber文件、IPC网表、坐标文件、装配PDF文件导出打包 一、Gerber文件层叠与参数设置二、装配图文件设置导出三、光绘参数设置四、Gerber孔符图、钻孔表及钻孔文件输出五、输出Gerber文件六、输出IPC网表七、导出坐标文件八、文件打包 一、Gerber文件层叠与参数设置…

WHAT - NodeJS 基本使用

目录 hello worldhttphttp requestaxios 版本 events: EventEmittererror catchconsoleNODE_ENVbufferfilefile openfile pathfile read and write streamos hello world const http require(http); const hostname 127.0.0.1; const port 3000; const server http.create…

沙普利值是什么,和沙普利值相结合的更好办法

目录 沙普利值是什么,应用场景有什么,举例说明 在云计算资源分配中举例 场景设定 用户需求和资源使用情况 沙普利值在资源分配中的应用(按需定价,) 归纳 和沙普利值相结合的更好办法 AHP法(层次分析法) ANP法(网络层次分析法) 模糊数学 沙普利值计算举例 沙…

一文讲清:生产报工系统的功能、报价以及如何选择

最近这几年,企业越来越注重生产的速度和成本,尤其是“性价比”,生产报工系统已经变成了制造业里不可或缺的一部分。不过,市场上生产报工系统的选择太多,价格也都不一样,这就给很多企业出了个难题&#xff1…

【笔记】【Git】多个dev分支合并到master分支的文件冲突

问题描述 多个dev分支在同步开发,同时发起代码评审,但合入master的时候存在先后顺序,那么后面同文件的操作则会提示“合并有文件冲突”,导致代码无法入库,只能重新提交。 在个人分支中如何解决与master分支差异&#…

最短路问题详解

一、引言 最短路问题(Shortest Path Problem)是计算机科学和运筹学中的一类重要问题。它通常用于解决网络中两个节点之间的最短距离或最低成本路径问题。这类问题在交通规划、通信网络、地图导航等领域有着广泛的应用。本文将详细介绍最短路问题的基本概…

指针的深入理解(3)(包括数组名的理解、一维数组传参的本质以及指针数组的相关知识及使用)

文章目录 1 数组名的理解2 使用指针访问数组3 一维数组传参的本质4 指针数组5 指针数组的使用 1 数组名的理解 当我们运行以下代码&#xff1a; #include <stdio.h> int main() {int arr[10] { 0 };printf("%p\n", &arr[0]);printf("%p\n", a…

护网设备的使用

设备概念 IPS IPS代表入侵防御系统&#xff08;Intrusion Prevention System&#xff09;&#xff0c;它不仅可以检测入侵行为&#xff0c;还可以主动采取措施进行防御。 IDS IDS代表入侵检测系统&#xff08;Intrusion Detection System&#xff09;&#xff0c;它通过监视网…

团体标准的发布主体的区别

团体标准的发布机构 团体标准是依法成立的社会团体&#xff0c;如学会、协会、商会、联合会、产业技术联盟等&#xff0c;为满足市场和创新需求&#xff0c;协调相关市场主体共同制定的标准。这些社会团体一般是民间组织&#xff0c;不属于政府的管理机构&#xff0c;而是政府…

前端开发中的热更新原理

一、什么是热更新 热更新&#xff08;Hot Module Replacement&#xff0c;HMR&#xff09;是一种在前端开发中极为重要的技术。它允许开发者在不重新加载整个页面的情况下&#xff0c;实时更新应用程序中的某些模块。简单来说&#xff0c;热更新能让你在开发过程中即时看到代码…

HCIA6以太网基础基于MAC划分VLAN

&#xff08;简写的命令可以敲Tab按键补全剩余&#xff09; 1.组网需求 场景&#xff1a;公司的网络中&#xff0c;管理者将同一部门的员工划分到VLAN10。要求只有本部门员工的PC接入才能互访&#xff0c;其他PC接入交换机属于其他VLAN&#xff08;666&#xff09;。可以配置…

遇到Windows无法启动时不要担心,这里有解决办法

序言 如果有一天你打开电脑,Windows拒绝启动,你该怎么办?其实“Windows无法启动”是一种常见症状,原因多种多样,因此你需要进行一些故障排除。 现代版本的Windows更善于从这种情况中自动恢复,而Windows XP遇到此问题时可能会停止在运行的地方,现代版本的Windows将尝试…

自然语言处理(NLP)教学解决方案

前言 自然语言处理&#xff08;NLP&#xff09;是计算机科学、人工智能以及语言学的交叉学科&#xff0c;它致力于使计算机能够理解、解释并生成人类自然语言&#xff0c;从而实现人机间有效沟通。近年来&#xff0c;随着深度学习技术的突破&#xff0c;自然语言处理技术在机器…

【复旦邱锡鹏教授《神经网络与深度学习公开课》笔记】神经元和人工神经网络

神经元 生物神经元&#xff1a; 平时处于抑制状态&#xff0c;当接受信息量达到一定程度后进入兴奋状态。 人工神经元&#xff1a; 一个人工神经元大致有两个步骤&#xff1a; 一是收集信息&#xff0c;如上图中 x 1 , ⋯ , x d x_1,\cdots,x_d x1​,⋯,xd​表示神经元可…

SinoDB导入导出工具汇总

在进行数据迁移、数据库表备份、表重建以及批量数据加载时&#xff0c;我们经常希望数据处理过程能够更快点。本文是SinoDB导入导出工具的汇总&#xff0c;大家可以根据不同场景选择合适的SinoDB导入导出工具。 1. 各工具特点 通常利用dbschema工具导出数据库结构&#xff0c;…

vivado HW_SIO_PLL

HW_SIO_PLL 描述 对于具有千兆位收发器&#xff08;GT&#xff09;的Xilinx FPGA设备&#xff0c;每个串行收发器通道 具有称为信道PLL&#xff08;CPLL&#xff09;的环形锁相环&#xff08;PLL&#xff09;。对于Xilinx UltraScale和7 系列FPGA&#xff0c;GTX每四路有一个额…