Vue响应式系统分支切换与cleanup - 清除遗留的副作用函数

文章目录

  • 前言
  • 分支切换与cleanup
    • 分支切换的问题
    • 依赖集合的收集
    • cleanup的实现
    • 完整的代码展示

前言

本篇文章代码思路来自 Vue3.0 源码, 部分理解来源于霍春阳 《Vue.js设计与实现》这本书的理解, 感兴趣的小伙伴可以自行购买阅读。可以非常明确的感受到作者对 Vue 的深刻理解以及用心, 富含非常全面的 Vue 的知识点, 强烈推荐给大家。

接上文
Vue响应式原理和本质 - 实现一个完善的响应式系统

分支切换与cleanup

前文回顾

在上一篇文章中, 我们实现了一个基本的响应式系统代码如下, 实现过程可以根据自己的需要进行阅读, 链接给到大家: http://lanan.blog.csdn.net/article/details/134127326。但是目前实现的响应式系统仍然存在一些问题, 本文针对分支切换产生的问题继续完善响应式系统

// 前文已实现的响应式系统const data = { name: "chenyq", age: 18 };
const bucket = new WeakMap();
const obj = new Proxy(data, {get(target, key) {// 将副作用函数收集到桶中track(target, key);// 返回属性值return target[key];},set(target, key, newVal) {// 设置属性值target[key] = newVal;// 从桶中取出副作用函数执行trigger(target, key);},
});function track(target, key) {// 没有activeEffect, 直接returnif (!activeEffect) return target[key];// 根据target从桶中取出depsMaplet depsMap = bucket.get(target);// 如果depsMap不存在, 那么就需要创建一个depsMap与之关联if (!depsMap) bucket.set(target, (depsMap = new Map()));// 再根据key, 从depsMap中取出deps, deps是一个Set集合, 里面存放的是与当前key相关的所有副作用函数let deps = depsMap.get(key);// 如果deps不存在, 则创建一个deps, 并将其添加到depsMap中if (!deps) depsMap.set(key, (deps = new Set()));// 最后将当前激活的副作用函数添加到桶里deps.add(activeEffect);
}function trigger(target, key) {// 根据target从桶中取出depsMapconst depsMap = bucket.get(target);if (!depsMap) return;// 取出与key相关的副作用函数const effects = depsMap.get(key);// 执行副作用函数effects && effects.forEach((fn) => fn());
}let activeEffect;
function effect(fn) {activeEffect = fn;fn();
}// 测试部分
// 执行副作用函数, 触发读取
effect(() => {document.body.innerText = obj.name;
});// 1秒后对obj.name属性进行修改
setTimeout(() => {obj.name = "abc";// obj.age = 19;// obj.notExist = "abc";
}, 1000);

分支切换的问题

首先, 我们需要知道分支切换会给我们上面实现的响应式系统带来哪些问题, 如有下面一段代码, effectFn 函数内部存在一个三元表达式, 当字段 obj.flag 发生变换时, 代码所需要执行的分支也会跟随变化。

const data = { flag: true, text: "分支切换问题" };
const obj = new Proxy(data, { /* ... */ });
function effect(fn) { /* ... */ }effect(function effectFn() {document.body.innerText = obj.flag ? obj.text : "not fount";
});

上面的分支切换会产生遗留的副作用函数, 例如上面代码中, 当 obj.flag 为 true 的时候, 会读取 obj.text 属性的值, 此时就会触发 flag 和 text 属性的 get 操作, 对应的会跟踪 flag 和 text 属性所依赖的副作用函数 effectFn。如下所示, effectFn 分别被字段 flag 和 text 所对应的依赖集合进行收集:

data└── flag└── effectFn└── text└── effectFn

当我们将 obj.flag 的值修改为 false 时, 会触发 obj.flag 的 set 操作, 会重新执行 effectFn 函数, 但由于此时 obj.flag 的值为 false , 不会读取 obj.text 属性。所以我们期望副作用函数 effectFn 不被 obj.text 对应的依赖集合进行收集, 此时副作用函数 effectFn 与响应式建立的关系如下:

data└── flag└── effectFn

但按照我们目前的实现, 无法做到这一点, 在我们将 obj.flag 修改为 false, 并重新执行了副作用函数 effectFn 后, 它们之间对应的依赖关系任然为之前的样子, 这也就产生了副作用函数的遗留问题, 这个问题会导致不必要的更新, 如用下面代码来举例:

调用 effect 函数会执行一次 effectFn 函数, 打印一次 “effect is running”

const data = { flag: true, text: "分支切换问题" };
const obj = new Proxy(data, { /* ... */ });
function effect(fn) { /* ... */ }effect(function effectFn() {console.log("effect is running");document.body.innerText = obj.flag ? obj.text : "not fount";
});

我们将 obj.flag 修改为 false 后, 会触发 obj.flag 的 set 操作, 即副作用函数 effectFn 会重新执行, 打印第二次 “effect is running”

setTimeout(() => {obj.flag = false;
}, 1000);

若此时我们继续修改 obj.text, 可以发现打印了第三次 “effect is running”, 说明 obj.text 对应的依赖集合中, 仍然保留了 effectFn 函数

obj.text = "测试分支切换";

我们想要实现的是, 当 obj.flag 为 false 的时候, 由于此时不在读取 obj.tex t属性, 那么该情况下 obj.text 无论如何进行变换, 都不应该重新执行副作用函数 effectFn 。目前的情况就是, obj.flag 为 false 的时候, 只要修改了 obj.text 的值, 就会重新执行副作用函数 effectFn, 归根结底还是因为副作用函数遗留产生的问题。

依赖集合的收集

其实解决这个问题的思路很简单, 我们只需要每次执行副作用函数的时候, 把这个副作用函数从所有被关联的依赖集合中删除, 比如上文中的建立的对应关系, obj.flag 和 obj.text 都和副作用函数 effectFn 相关联, 我们就可以在执行副作用函数 effectFn, 将它与 obj.flag 和 obj.text 的关系都断开, 如下所示:

data└── flag└── text

待副作用函数执行完成之后, 我们再重新建立联系, 这样一来新建立的联系中就不会包含遗留的副作用函数。那么现在的问题就是, 想要将一个副作用函数从所有与之关联的依赖集合中移除, 我们需要明确的知道哪些依赖集合包含这个副作用函数。这样的话我们就需要对 effect 函数进行重新设计, 我们在 effect 函数中, 定义一个新的函数 effectFn, 将定义的 effectFn 函数设置为当前激活的副作用函数, 并为这个 effectFn 函数添加 deps 属性, deps 属性为一个数组, 数组中存放当前副作用函数的依赖集合, 代码如下所示:

let activeEffect;
function effect(fn) {// 定义一个effectFn函数const effectFn = () => {// 将定义的effectFn函数设置为当前激活的副作用函数activeEffect = effectFn;fn();};// deps属性中用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = [];effectFn();
}

那么我们只需要在 track 函数中, 将当前副作用函数的依赖集合添加到 effectFn.deps 数组中, 就可以完成副作用函数相关联的依赖集合的收集, 如下代码所示:

function track(target, key) {if (!activeEffect) return target[key];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()));deps.add(activeEffect);// 将依赖集合收集到activeEffect.deps数组中activeEffect.deps.push(deps)
}

cleanup的实现

有了上面的依赖集合收集之后, 我们就可以做到每次在副作用函数执行时, 根据 effectFn.deps , 将副作用函数从所有依赖集合中移除:

let activeEffect;
function effect(fn) {const effectFn = () => {// 调用cleanup函数完成清除cleanup(effectFn)activeEffect = effectFn;fn();};effectFn.deps = [];effectFn();
}

cleanup 函数接收一个副作用函数作为参数, 遍历 effectFn.deps 数组, 数组中每一项都是一个依赖集合, 依次从每一个依赖集合中将副作用函数 effectFn 移除, 实现代码如下所示:

function cleanup(effectFn) {// 遍历effectFn.deps数组const length = effectFn.deps.length;for (let i = 0; i < length; i++) {// 取出依赖集合const deps = effectFn.deps[i];// 将副作用函数从依赖集合中移除deps.delete(effectFn);}// 将effectFn.deps重置为空数组effectFn.deps.length = 0;
}

完成上面操作, 我们的响应式系统就已经可以避免副作用函数的遗留问题了, 但是上面代码运行会出现一个无限循环, 导致死循环的原因就出现在 trigger 函数中:

function trigger(target, key) {const depsMap = bucket.get(target);if (!depsMap) return;const effects = depsMap.get(key);effects && effects.forEach((fn) => fn()); // 这行代码有问题
}

我们来分析一下问题的原因, 在 trigger 内部, 我们遍历的 effects, 它是一个 Set 集合, 里面存储着副作用函数; 我们遍历这个 Set 集合时, 会取出每一个副作用函数进行执行, 在执行副作用函数时会调用 cleanup 函数进行清除, 会将这个副作用函数从 Set 集合中移除; 但是我们继续执行函数, 会导致它又重新被收集到 Set 集合当中, 而此时 Set 集合遍历任在进行。

语言规范中对此有明确的说明: 在调用 forEach 遍历 Set 集合时, 如果一个值已经被访问过了, 但该值被删除并重新添加到集合, 如果此时 forEach 遍历没有结束, 那么该值会重新被访问。

例如下面代码, Set 集合当中只有一个元素, 在遍历时我们将这个元素先删除, 再添加; 那么该值就会被重新访问, 进而这个循环就会无限执行进行下去:

const set = new Set([1])set.forEach(item => {set.delete(1)set.add(1)
})

因此 trigger 中的代码同理, 也会无限的执行下去。解决办法很简单, 我们可以构造另外一个 Set 集合并遍历它, 这样就不会无限的执行下去了。具体做法: 由于我们将副作用函数移除和添加的函数是 effects, 那么我们就不要直接遍历 effects, 创建一个新的集合 effectsToRun 代替直接遍历 effects, 代码如下所示:

function trigger(target, key) {const depsMap = bucket.get(target);if (!depsMap) return;const effects = depsMap.get(key);// 创建一个新的Set集合用来遍历const effectsToRun = new Set(effects)// 执行副作用函数effectsToRun.forEach(effectFn => effectFn())
}

完整的代码展示

到这里我们就彻底解决了副作用函数产生遗留的问题, 到这里我们实现的响应式系统的完整代码给到大家:

const data = { flag: true, text: "分支切换问题" };
const bucket = new WeakMap();
const obj = new Proxy(data, {get(target, key) {// 将副作用函数收集到桶中track(target, key);// 返回属性值return target[key];},set(target, key, newVal) {// 设置属性值target[key] = newVal;// 从桶中取出副作用函数执行trigger(target, key);},
});// 依赖收集
function track(target, key) {// 没有activeEffect, 直接returnif (!activeEffect) return target[key];// 根据target从桶中取出depsMaplet depsMap = bucket.get(target);// 如果depsMap不存在, 那么就需要创建一个depsMap与之关联if (!depsMap) bucket.set(target, (depsMap = new Map()));// 再根据key, 从depsMap中取出deps, deps是一个Set集合, 里面存放的是与当前key相关的所有副作用函数let deps = depsMap.get(key);// 如果deps不存在, 则创建一个deps, 并将其添加到depsMap中if (!deps) depsMap.set(key, (deps = new Set()));// 最后将当前激活的副作用函数添加到桶里deps.add(activeEffect);// 将依赖集合收集到activeEffect.deps数组中activeEffect.deps.push(deps);
}// 派发更新
function trigger(target, key) {// 根据target从桶中取出depsMapconst depsMap = bucket.get(target);if (!depsMap) return;// 取出与key相关的副作用函数const effects = depsMap.get(key);// 创建一个新的Set集合用来遍历const effectsToRun = new Set(effects);// 执行副作用函数effectsToRun.forEach((effectFn) => effectFn());
}let activeEffect;
function effect(fn) {// 定义一个effectFn函数const effectFn = () => {// 调用cleanup函数完成清除cleanup(effectFn);// 将定义的effectFn函数设置为当前激活的副作用函数activeEffect = effectFn;fn();};// deps属性中用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = [];effectFn();
}// 清除依赖
function cleanup(effectFn) {// 遍历effectFn.deps数组const length = effectFn.deps.length;for (let i = 0; i < length; i++) {// 取出依赖集合const deps = effectFn.deps[i];// 将副作用函数从依赖集合中移除deps.delete(effectFn);}// 将effectFn.deps重置为空数组effectFn.deps.length = 0;
}// 测试部分
// 执行副作用函数, 触发读取
effect(function effectFn() {console.log("effect is running");document.body.innerText = obj.flag ? obj.text : "not fount";
});// 1秒后对obj.name属性进行修改
setTimeout(() => {obj.flag = false;obj.text = "测试分支切换";
}, 1000);

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

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

相关文章

每天写java到期末考试(6.6)-java文件输入输出流实验

1、用字节流读写二进制文件 要求:用DataOutputStreamFileOutputStream类将1,2,…,100,这100个数字写入到文件 d:\out1.bin里,然后再用DatalnputStreamFilelnputStream类将d:\out1.bin的内读出来,并输出到屏幕上。 用DataOutputStreamFileOutputStream写入二进制数据时,直接调…

单元测试AIR原则:提升代码质量的秘密武器

文章目录 引言一、AIR原则1. Automatic&#xff08;自动化&#xff09;2. Independent&#xff08;独立性&#xff09;3. Repeatable&#xff08;可重复性&#xff09; 二、Automatic&#xff08;自动化&#xff09;三、Independent&#xff08;独立性&#xff09;四、Repeatab…

【MySQL】sql语句之表操作(上)

序言 在上一篇的数据库操作的内容中&#xff0c;学习了两种属性和常用的七种操作&#xff0c;学习是循序渐进的&#xff0c;库的操作学完了&#xff0c;就要开始学习表的操作了&#xff0c;而表可与数据强相关&#xff0c;比如DDL&#xff0c;即数据定义语言&#xff0c;DML&am…

DVWA-XSS(Stored)

Low 观察后端代码&#xff0c;对输入进行了一些过滤和转义。trim(string,charlist) 函数用于移除字符串两侧的空白字符或其他预定义字符&#xff0c;charlist 参数可以规定从字符串中删除哪些字符。stripslashes() 函数用于删除反斜杠。mysqli_real_escape_string() 函数用于对…

SAAS系统架构设计剖析

多租户数据隔离 用户担心数据安全性&#xff0c;也就是要做数据隔离&#xff0c;不允许 A 租户查到 B 租户的数据 1、软隔离 数据在一起&#xff0c;只不过带着租户 id 查询 在底层驱动 jar 上进行封装&#xff0c;强制带上租户 id 比如&#xff1a;MySQL、MQ、Redis&#…

【论文精读】DCRNN-扩散图卷积循环神经网络

DCRNN 模型是南加州大学的 Li 等人发表在 I C L R 2018 ICLR 2018 ICLR2018 会议上一个用于交通预测的时空预测模型,论文题目为: 《DIFFUSION CONVOLUTIONAL RECURRENT NEURAL NETWORK: DATA-DRIVEN TRAFFIC FORECASTING》,文章地址为: https://arxiv.org/abs/1707.01926。 …

vs中运行程序时,报不能运行解决方式

问题 在vs中编译运行程序中&#xff0c;如果程序还在运行&#xff0c;编译会报错&#xff0c;但是在后台又找不到对应的程序 解决方式 1、tasklist | find “进程名” 2、taskkill /PID

【实战】kafka3.X kraft模式集群搭建

文章目录 前言kafka2.0与3.x对比准备工作JDK安装kafka安装服务器增加hosts 修改Kraft协议配置文件格式化存储目录 启动集群停止集群测试Kafka集群创建topic查看topic列表查看消息详情生产消息消费消息查看消费者组查看消费者组列表 前言 相信很多同学都用过Kafka2.0吧&#xf…

二叉树的镜像--c++【做题记录】

【问题描述】 给定扩展二叉树的前序序列&#xff0c;构建二叉树。 求这课二叉树的镜像&#xff0c;并输出其前序遍历序列。 【输入形式】 输入扩展二叉树的前序序列。 【输出形式】 输出镜像二叉树的前序遍历序列。 【样例输入】 ab##cd##e## 【样例输出】 镜像后二叉树的前序遍…

功能问题:如何防止接口重复请求?

大家好&#xff0c;我是大澈&#xff01; 本文约 1400 字&#xff0c;整篇阅读约需 3 分钟。 防止接口重复请求在软件开发中非常重要&#xff0c;重复请求必然会导致服务器资源的浪费。 因为每次请求都需要服务器进行处理&#xff0c;如果请求是重复的&#xff0c;那么服务…

乘船过河(ship)

合肥市第33届信息学竞赛&#xff08;2016年&#xff09; 题目描述 Description 卡卡西和小朋友们要乘船过河了&#xff0c;港口有很多条船可以租到&#xff0c;并且之间没有区别&#xff0c;每条船的出租费用也是一样的。但是一条船最多只能乘坐两个人&#xff0c;且乘客的总…

STM32 IIC 使用 HAL 库操作eeprom

在STM32上通过I2C接口&#xff08;注意&#xff1a;在标准STM32库中&#xff0c;I2C接口通常被写为"I2C"而不是"IIC"&#xff09;与EEPROM芯片通信时&#xff0c;你需要遵循I2C通信协议&#xff0c;并使用STM32的HAL库或标准外设库&#xff08;如果适用&am…

tomcat配置请求的最大参数个数和请求数据大小

maxParameterCount"10000" maxPostSize"10485760" maxParameterCount&#xff1a;单个请求最大请求参数个数&#xff1b; maxPostSize&#xff1a;单个请求最大数据大小&#xff0c;1048576010M&#xff1b;

基本算法——位运算

a^b 原题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 题目描述 运行代码 #include<iostream> using namespace std; long long a,b,c,t1; int main() {cin>>a>>b>>c;for(;b;b/2){if(b&1)tt*a%c;aa*a%c;}cout<<t%c; } 代码思路…

汽车软件 OTA技术解析

汽车软件 OTA 技术概述 在当今汽车行业中,软件定义汽车的概念逐渐深入人心。随着汽车智能化和网联化的发展,汽车软件的重要性日益凸显。而汽车软件 OTA(Over-the-Air)技术作为一种重要的软件升级和维护方式,正逐渐成为汽车行业的热点话题。 汽车软件 OTA 技术是指通过无线…

逻辑回归及python实现

概述 logistic回归是一种广义线性回归&#xff08;generalized linear model&#xff09;&#xff0c;因此与多重线性回归分析有很多相同之处。它们的模型形式基本上相同&#xff0c;都具有 w‘xb&#xff0c;其中w和b是待求参数&#xff0c;其区别在于他们的因变量不同&#x…

App Inventor 2 复制屏幕功能,界面设计更便捷,避免误删组件

“复制屏幕”功能全新上线&#xff0c;中文网独有&#xff08;MIT没有此功能&#xff09;&#xff0c;可以复制屏幕中的普通组件、图片、附件、拓展、代码块。更多升级详情可查看发布日志。 下面演示一下屏幕的复制效果&#xff1a; 1、Screen1屏幕上有若干组件、及一个SQLit…

美业SaaS系统源码分享-收银管理的主要功能

美业SaaS系统 连锁多门店美业收银系统源码 多门店管理 / 会员管理 / 预约管理 / 排班管理 / 商品管理 / 活动促销 PC管理后台、手机APP、iPad APP、微信小程序 ▶ 博弈美业-收银管理功能 1、同时支持支付宝和微信支付&#xff0c;具有简单便捷安全等优点&#xff0c;并且符…

MySQL之查询性能优化(八)

查询性能优化 MySQL查询优化器的局限性 MySQL的万能"嵌套循环"并不是对每种查询都是最优的。不过还好&#xff0c;MySQL查询优化器只对少部分查询不适用&#xff0c;而且我们往往可以通过改写查询让MySQL高效地完成工作。还有一个好消息&#xff0c;MySQL5.6版本正…

Java开发注意事项

注意&#xff1a;测试类中使用Autowired注解注入Bean&#xff0c;不要使用RequiredArgsConstructor注解注入Bean 正确示范: import org.springframework.boot.test.context.SpringBootTest; import org.springframework.beans.factory.annotation.Autowired;SpringBootTest c…