从单测入手,完善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 trigger
  obj.foo++;
  expect(scheduler).toHaveBeenCalled();
  // should not run yet
  expect(dummy).toBe(1);
  // manually run
  run();
  // should have run
  expect(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 callable
  runner();
  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 callable
  runner();
  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);
});

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Power Automate-创建一个power Apps使用的流

创建即时云端流,选择Power Apps

慢SQL治理经验总结

慢SQL的定义,执行超过1s的SQL为慢SQL。 1.慢SQL导致的后果: 系统的响应时间延迟,影响用户体验。资源占用增加,增高了系统的负载,其他请求响应时间也可能会收到影响。慢SQL占用数据库连接的时间长,如果有大量慢SQL查询…

盘点72个ASP.NET Core源码Net爱好者不容错过

盘点72个ASP.NET Core源码Net爱好者不容错过 学习知识费力气,收集整理更不易。 知识付费甚欢喜,为咱码农谋福利。 链接:https://pan.baidu.com/s/1nlQLLly_TqGrs5O8eOmZjA?pwd8888 提取码:8888 项目名称 (Chinese) 物业收费…

城市内涝监测仪的作用有哪些?

城市内涝近几年愈发频繁,它的出现不仅仅会导致财产损失,还可能危及公共安全。所以对路面积水进行实时监测刻不容缓。内涝积水监测仪的早期警报系统,有助于提高城市的紧急响应能力。政府远程监控城市路面水位,实现精准的系统化管理…

PBHA(page based hardware attributes)的介绍

基本介绍 基于页面的硬件属性 (PBHA:page based hardware attributes) 是一项可选的、由实现定义的功能。 它允许软件在转换表中设置最多四位,然后通过事务通过内存系统传播这些位,并可在系统中用于控制系统组件。这些位的含义特定于系统设计…

什么是应用集成?应用集成快速指南

什么是应用集成? 想象一下,在剧院观看音乐剧,没有人站在正确的地方,每个人都在互相交谈,或者有漫长而尴尬的沉默,管弦乐队的音乐家们在错误的时刻演奏,完全是混乱的,就会很难看。 业…

做C语言的编程题总是想骂人怎么办?

做C语言的编程题总是想骂人怎么办? 可能C语言的编程题难住了您吧,导致情绪激烈不平静,那么做C语言的编程题可以顺利-些吗? 当然有一些方法可是现实此目标的:最近很多小伙伴找我,说想要一些C语言的资料,然后我根据自己…

ARPG----C++学习记录05 Section12 动画蒙太奇,收拿剑,MetaSound,调整动画

代码更新 https://github.com/BAOfanTing/ARPG_Game_Code/commit/c629270e49496ba1bcbaf03780d23c1842ca5e7a Animation Montages动画蒙太奇 蒙太奇的工作流程 新建一个鼠标左键的按键映射,下载一些攻击动画,重定向给我们的人物,新建一个动画…

【多样注释】刚入职就崩溃!这段神秘注释让人无法忍受

最近写码写的头晕脑涨,同事听完后给我发来几张神图,我看完当场笑尿🤣,分享一下,权当博君一笑了。 代码注释图案传送门 1、看到了这个注释,心凉了一半 2、阅读源码的人,心里一定的崩溃的 3、这…

SwiftUI - 界面布局知识点

前言 SwiftUI采用的布局方式是和Flutter一样是弹性布局,而不是iOS之前的坐标轴的方式布局,不用准确的设置出位置大小,只需要设置当前视图大小及视图间排布的方式。灵活性增强,布局操作简便,SwiftUI与Flutter布局原理一…

Flowable串行流程移动活动

在Activiti6和Activiti7中跳转节点都要自己实现,Flowable增加了这个功能。 一:简介 串行流程是指简单的一条线的流程,流程中如果包含会签、排它网关也算串行流程。 节点移动有三种方式: 向前移动foreward。向后移动back。直接跳…

一文看懂香港优才计划和高才通计划的区别和优势?如何选?

一文看懂香港优才计划和高才通计划的区别和优势?如何选? 为什么很多人都渴望有个香港身份? 英文这里和内地文化相近,语言相通,同时税率较低、没有外汇管制,有稳定金融体制和良好的营商环境,诸多…

java入门, 记录检测网络

一、需求 在开发中,我们经常需要本地连接服务器,或者数据库这些机器或者组件,但是有时候网络不通,我们怎样检测,除了ping 和 telnet 还需要那些常用的技能。 二、检测网络 1、一般我们先ping一些需要连接的网络ip 或…

WY-35A4三相欠压继电器 导轨安装,延时动作0-99.99s可调

系列型号 单相 JY-45A1电压继电器;JY-45B1电压继电器; JY-45C1电压继电器;JY-45D1电压继电器; JY-41A1电压继电器;JY-41B1电压继电器; JY-41C1电压继电器;JY-41D1电压继电器; …

企业数字化建设之——老板关注的IT指标有哪些 ?

投资回报ROI | 商业价值 | 系统可用性 | 业务的参与程度 | 技术债务指数 降本,增效是IT部门工作的永恒话题 ,降低成本 ,增加效益 ,降本增效的工作方向: 1 年初KPI目标、目标完成情况、关键证据、公司主线工作…

主流接口测试框架对比

📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢交流讨论:欢迎加入我们一起学习!📢资源分享:耗时200小时精选的「软件测试」资…

基于SSM的供电所档案管理系统

末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用JSP技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…

前端入门(二)js速成与vue脚手架搭建

文章目录 JS常用API操作对象操作对象增删改查js深拷贝、浅拷贝js实现深拷贝的方式 安全访问 JS常用API操作 对象操作 对象增删改查 创建对象 let obj {}新增属性 obj.a 1 、obj[a] 1修改属性 obj.a ‘a’查询属性 obj.a 、obj[a]删除属性 delete obj.a js深拷贝、浅拷贝…

【无线网络技术】——无线传输技术基础(学习笔记)

目录 🕒 1. 无线传输媒体🕘 1.1 地面微波🕘 1.2 卫星微波🕘 1.3 广播无线电波🕘 1.4 红外线🕘 1.5 光波 🕒 2. 天线🕘 2.1 辐射模式🕘 2.2 天线类型🕤 2.2.1 …

Power Automate-与Microsoft Forms连接

创建自动化云端流,流的触发器选择第一个提交新回复时 点击蓝色的Change connection,登录创建Microsoft Forms表单的账号 选择提前创建的表单;如果想连接其他账号创建的Microsoft Forms表单,可以再次点击蓝色的Change connection&a…