从单测入手,完善Vue3源码中底层API effect功能

基于上一篇文章中实现的effect方法,根据 Vue3 源码中单测,完善该方法的三点功能,分别是:

  1. runner: effect可以返回自执行的入参runner函数
  2. scheduler: effect支持添加第二个参数选项中的scheduler功能
  3. stop: effect添加stop功能

runner

单测

effect.spec.ts文件中添加关于runner的测试用例。

it("should be return runner when call effect", () => {let foo = 1;const runner = effect(() => {foo++;return "foo";});expect(foo).toBe(2);const r = runner();expect(foo).toBe(3);expect(r).toBe("foo");
});

上面测试用例的意思是,effect内部的函数会自执行一次,foo的值变成2。effect是一个可执行函数runner,执行runnereffect内部函数也会执行,因此foo的值会再次自增变成3,并且runner的返回值就是effect内部函数的返回值。

实现

effect函数需要可以返回它的入参执行函数,且内部执行函数可以返回。

class ReactiveEffect {private _fn: any;constructor(fn) {this._fn = fn;}run() {reactiveEffect = this;return this._fn();}
}export function effect(fn) {let _effect = new ReactiveEffect(fn);_effect.run();const runner = _effect.run.bind(_effect)return runner;
}

需要注意的是,这里存在this指向的问题,在返回_effect.run函数时需要绑定当前实例。

验证

执行yarn test effect

scheduler

单测

it("scheduler", () => {let dummy;let run: any;const scheduler = jest.fn(() => {run = runner;});const obj = reactive({ foo: 1 });const runner = effect(() => {dummy = obj.foo;},{scheduler,});expect(scheduler).not.toHaveBeenCalled();expect(dummy).toBe(1);// should be called on first triggerobj.foo++;expect(scheduler).toHaveBeenCalled();// should not run yetexpect(dummy).toBe(1);// manually runrun();// should have runexpect(dummy).toBe(2);
});

上面测试用例代码的意思是:effect方法接收第二个参数,是一个选项列表对象,其中有一个是scheduler,是一个函数。这里用jest.fn模拟了一个函数将变量run赋值成runner函数。在第一次执行的时候,scheduler函数不调用执行,effect的第一个参数函数自执行,所以dummy赋值为1;当响应式对象变化时,也就是obj.foo++时,scheduler会被执行,但是dummy的值还是1,说明第一个参数函数并没有执行;run执行,也就是effect返回函数runner执行时,第一个参数函数执行,因为obj.foo++,所以dummy变成2。

可以总结出scheduler包含的需求点:

  1. 通过effect的第二个参数给定一个schedulerfn
  2. effect第一次执行的时候,执行第一个参数function
  3. 当响应式对象触发set操作时,不会执行function,而执行scheduler
  4. 当执行runner时,会再次执行function

实现

首先是effect函数可以接收第二个对象参数。

export function effect(fn, options: any = {}) {let _effect = new ReactiveEffect(fn, options.scheduler);_effect.run();const runner = _effect.run.bind(_effect)return runner;
}

Class类中也要相应的接收scheduler

class ReactiveEffect {private _fn: any;public scheduler: Function | undefined;constructor(fn, scheduler) {this._fn = fn;this.scheduler = scheduler;}run() {reactiveEffect = this;return this._fn();}
}

当响应式对象触发set操作时,也就是触发依赖时,在trigger方法中,执行scheduler,只需要判断是否存在scheduler,存在即执行。

export function trigger(target, key) {let depMap = targetMap.get(target);let dep = depMap.get(key);for (const effect of dep) {if (effect.scheduler) {effect.scheduler();} else {effect.run();}}
}

验证

stop

单测

import { effect, stop } from "../reactivity/effect";it("stop", () => {let dummy;const obj = reactive({ prop: 1 });const runner = effect(() => {dummy = obj.prop;});obj.prop = 2;expect(dummy).toBe(2);stop(runner);obj.prop = 3;expect(dummy).toBe(2);// stopped effect should still be manually callablerunner();expect(dummy).toBe(3);
});it("onStop", () => {const onStop = jest.fn();const runner = effect(() => {}, { onStop });stop(runner);expect(onStop).toHaveBeenCalled();
});

stop功能有两个测试用例,对应不同的功能,我们逐个分析。

"stop"中,effect内函数自执行一次,所以第一次断言dummy为上面赋值的2;执行stop方法,stop方法是来自effect对外暴露的方法,它接收runner函数作为参数,即便再更新响应式对象,effect内函数也不执行,dummy仍然是2;再次执行runner,恢复执行effect内函数,dummy变成了3。

总结来说,stop可以阻止effect内函数执行。

"onStop"中,effect函数接收第二个参数对象中有个属性是onStop,且接收一个函数,当执行stop时,onStop函数会被执行。

实现

触发依赖时,trigger方法中循环执行了dep中所有的effect内方法,那需要阻止执行,就可以从dep中删除该项。

首先stop方法接收runner函数作为参数。

export function stop(runner) {runner.effect.stop();
}

runner函数上挂载一个effect实例,就可以获取到 Class 类中定义的stop方法。

class ReactiveEffect {private _fn: any;public scheduler: Function | undefined;constructor(fn, scheduler) {this._fn = fn;this.scheduler = scheduler;}run() {reactiveEffect = this;return this._fn();}stop() {}
}export function effect(fn, options: any = {}) {let _effect = new ReactiveEffect(fn, options.scheduler);extend(_effect, options);_effect.run();const runner: any = _effect.run.bind(_effect);runner.effect = _effect; // 挂载effect实例return runner;
}

那如何从dep中删除需要阻止执行的一项呢?

track方法中dep.add(reactiveEffect)建立了dep这个Set结构和effect实例的关系,但是在 Class 类中并没有实例和dep的映射关系,因此可以Class类中定义一个deps数组用来存放该实例的所有dep,在需要调用stop方法时将删除dep中的该effect实例方法。

class ReactiveEffect {private _fn: any;public scheduler: Function | undefined;deps = []; constructor(fn, scheduler) {this._fn = fn;this.scheduler = scheduler;}run() {reactiveEffect = this;return this._fn();}stop() { this.deps.forEach((dep: any) => {dep.delete(this);});}
}export function track(target, key) {...dep.add(reactiveEffect);reactiveEffect.deps.push(dep); // 存放deps
}

验证

优化

虽然单测通过了,但是代码是有优化空间的,我们来重构一下。

stop方法中逻辑可以抽离成一个单独函数。

class ReactiveEffect {...stop() {cleanupEffect(this);}
}function cleanupEffect(effect) {effect.deps.forEach((dep: any) => {dep.delete(effect);});
}

性能上的优化,当用户一直调用stop方法,会导致这儿一直无故循环遍历,因此可以设置一个标志位来判断是否已经调用过执行了删除操作。

class ReactiveEffect {private _fn: any;public scheduler: Function | undefined;deps = [];active = true;constructor(fn, scheduler) {this._fn = fn;this.scheduler = scheduler;}run() {reactiveEffect = this;return this._fn();}stop() {if (this.active) {cleanupEffect(this);this.active = false;}}
}

重构后需要再次执行单测,确保没有破坏功能。

实现

来实现stop的第二个功能onStop

首先将onStop方法挂载effect实例上。

export function effect(fn, options: any = {}) {let _effect = new ReactiveEffect(fn, options.scheduler);_effect.onStop = options.onStop_effect.run();const runner: any = _effect.run.bind(_effect);runner.effect = _effect;return runner;
}

当执行stop时,onStop函数会被执行。

class ReactiveEffect {private _fn: any;public scheduler: Function | undefined;deps = [];active = true;onStop?: () => void;constructor(fn, scheduler) {this._fn = fn;this.scheduler = scheduler;}run() {reactiveEffect = this;return this._fn();}stop() {if (this.active) {cleanupEffect(this);if (this.onStop) {this.onStop();}this.active = false;}}
}

验证

优化

effect方法的第二个参数options可能存在很多选项,那每次都通过_effect.onStop = options.onStop挂载到实例上是不优雅的,因此可以抽离这块的逻辑,作为一个公共的方法。

在 src 下新建文件夹 shared,新建index.ts

export const extend = Object.assign;

那在effect中就可以使用extend方法更语义化表达。

export function effect(fn, options: any = {}) {let _effect = new ReactiveEffect(fn, options.scheduler);extend(_effect, options);_effect.run();const runner: any = _effect.run.bind(_effect);runner.effect = _effect;return runner;
}

重构完再次执行yarn test effect验证是否破坏功能。

验证

最后需要执行全部的单测,验证新增功能对原有代码是否有破坏,执行yarn test

在执行reactive单测时,出现了如上的报错,提示reactiveEffect可能是undefined不存在deps

reactive.spec.ts中只是单纯的测试了reactive的核心功能,此时还没有涉及到effect方法,reactiveEffect的赋值是在effect自执行时触发的,因此是初始undefined状态。

export function track(target, key) {...if (!reactiveEffect) return; // 边界处理dep.add(reactiveEffect);reactiveEffect.deps.push(dep);
}

最后再次验证,测试通过,功能完善成功。


2023/11/13更新

修改stop单测

在原本的基础上,修改effectstop测试用例。

it("stop", () => {let dummy;const obj = reactive({ prop: 1 });const runner = effect(() => {dummy = obj.prop;});obj.prop = 2;expect(dummy).toBe(2);stop(runner);// obj.prop = 3;obj.prop++;expect(dummy).toBe(2);// stopped effect should still be manually callablerunner();expect(dummy).toBe(3);
});

运行单测yarn test effect

报错分析

简单分析一下报错的原因。

obj.prop++可以理解成obj.prop = obj.prop + 1,存在getset两个操作,触发get操作会重新收集依赖,导致stopcleanupEffect方法删除所有effect失效。

实现

知道了根本原因是先触发get操作重新执行了effect中函数,也就是调用了track方法,那需要完善的逻辑应该这个方法入手。我们可以定义一个全局变量shouldTrack来判断是否需要进行track操作。

let reactiveEffect;
let shouldTrack;  // 定义export function track(target, key) {...if(!shouldTrack) return // 直接return不进行依赖收集if (!reactiveEffect) return;dep.add(reactiveEffect);reactiveEffect.deps.push(dep);
}

进行赋值的时候触发set操作,执行trigger函数,最终调用的是 Class 类ReactiveEffectrun方法。run方法中原本是直接返回了入参函数的执行结果,这里就需要判断一下stop的情况,可以依据active来判断。

如果是调用了stop方法之后,active赋值为false,这时候直接返回fn

如果没有调用stop方法,先将shouldTrack设为true,表示可以进行track调用,然后执行fn,并将执行结果返回,但是在返回之前需要重置操作,将shouldTrack设置成false,因为如果在遇到stop之后,run函数中会直接return,不会将shouldTrack设为true,那在track时,就会走!shouldTrack直接return不再收集依赖。

run() {if (!this.active) {return this._fn();}shouldTrack = true;reactiveEffect = this;const result = this._fn();shouldTrack = false;return result;
}

重构

trackshouldTrackreactiveEffect的边界判断,可以提到track函数体内顶部,单独封装一个函数合成这两个判断。

依赖收集这儿可以优化的点,当dep中存在的reactiveEffect就不再重复收集了。

export function track(target, key) {if (!isTracking()) return;...if (dep.has(reactiveEffect)) return;dep.add(reactiveEffect);reactiveEffect.deps.push(dep);
}function isTracking() {return shouldTrack && reactiveEffect !== undefined;
}

调试

修改一下单测,更简单的单测来通过调试清晰看一下上述流程。

it("stop", () => {let dummy;const obj = reactive({ prop: 1 });const runner = effect(() => {dummy = obj.prop;});stop(runner);obj.prop++;expect(dummy).toBe(1);
});

这里通过一个视频讲解来更形象的了解,视频详情查看

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

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

相关文章

LeetCode(17)罗马数字转整数【数组/字符串】【简单】

目录 1.题目2.答案3.提交结果截图 链接: 13. 罗马数字转整数 1.题目 罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 字符 数值 I 1 V 5 X 10 L …

修改 jar 包中的源码方式

在我们开发的过程中,我们有时候想要修改jar中的代码,方便我们调试或或者作为生产代码打包上线,但是在IDEA中,jar包中的文件都是read-only(只读模式)。那如何我们才能去修改jar包中的源码呢? 1.…

【中间件篇-Redis缓存数据库06】Redis主从复制/哨兵 高并发高可用

Redis高并发高可用 复制 在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求。Redis也是如此,它为我们提供了复制功能,实现了相同数据的多个Redis 副本。复制功能是高可用Re…

系列一、JVM的架构图

一、JVM的位置 JVM是运行在操作系统之上的,它与硬件没有直接的交互。 二、JVM的架构图

Python中的时间序列分析模型ARIMA

大家好,时间序列分析广泛用于预测和预报时间序列中的未来数据点。ARIMA模型被广泛用于时间序列预测,并被认为是最流行的方法之一。本文我们将学习如何在Python中搭建和评估用于时间序列预测的ARIMA模型。 ARIMA模型 ARIMA模型是一种用于分析和预测时间…

python科研绘图:绘制X-bar图

目录 1.X-bar 图的基本概念 2.X-bar 图的绘制过程 3.X-bar 图的优势 4.X-bar 图的绘制 1.X-bar 图的基本概念 X-bar控制图是一种统计工具,用于监控和控制生产过程中的质量变量。它是过程能力分析和统计过程控制(SPC,Statistical Process…

供应商选择和评估:如何寻找合适的供应商并与其合作

如果供应商不能按时交货或产品质量低劣,制造商的生产计划就会延误;客户交货将被延迟,商品可能被退回,你的公司声誉也将受损。 要在当今竞争激烈的市场中取得成功,你需要一流的、价格合理且来源可靠的原材料和服务&…

HTTP1.1协议详解

目录 协议介绍协议的特点存在的问题协议优化方案与HTTP 1.0协议的区别 协议介绍 HTTP 1.1是一种基于文本的互联网实体信息交互协议,是Web上任何数据交换和客户端-服务器交互的基础。它允许获取各种类型的资源,如HTML文档,并支持在互联网上交…

wps卸载和重新安装

卸载WPS sudo apt remove wps-office安装WPS 下载地址 安装命令 sudo dpkg -i wps-office_11.1.0.11708_amd64.debsunyuhuasunyuhua-HKF-WXX:~$ sudo dpkg -i wps-office_11.1.0.11708_amd64.deb 正在选中未选择的软件包 wps-office。 (正在读取数据库 ... 系统当前共安装…

企业APP软件定制开发的关键步骤|网站小程序搭建

企业APP软件定制开发的关键步骤|网站小程序搭建 在当今数字化快速发展的时代,企业越来越意识到拥有自己的APP软件对于提高业务效率和用户体验的重要性。然而,企业APP软件定制开发并不是一项简单的任务,它需要经过一系列关键步骤来确保最终的产…

【miniQMT实盘量化3】获取历史行情数据

前言 上篇文章,介绍了如何与miniQMT建立连接,这篇开始,我们会深入探讨miniQMT的每个功能接口。首先,从获取历史数据开始。 迅投的官方文档目前已经更新,miniQMT对应原生API部分 接口汇总 与历史行情数据相关的接口&a…

EfficientNet:通过模型效率彻底改变深度学习

一、介绍 EfficientNet 是深度学习领域的里程碑,代表了神经网络架构方法的范式转变。EfficientNet 由 Google Research 的 Mingxing Tan 和 Quoc V. Le 开发,在不影响性能的情况下满足了对计算高效模型不断增长的需求。本文深入探讨了 EfficientNet 背后…