likes
comments
collection
share

实现百行不到的 mitt 我学到了这些技术

作者站长头像
站长
· 阅读数 15

前言

本章是实现mitt,学习发布订阅的思想,然后包含了pnpm、搭建monorepo工程、typescript、vite、vitest,为了学习多种技术,混在一起

pnpm + monorepo

什么是 pnpm?

pnpm 是一种包管理工具,用于管理项目中的依赖项。与传统的包管理工具如 npm 或 yarn 不同,pnpm 采用了一种独特的依赖项管理策略,旨在减少磁盘空间的占用和加速依赖的安装。

这里我们列举一些 pnpm 的有点:

  • 磁盘空间效率: pnpm 的依赖管理方式避免了重复下载和存储相同的方式,节省了磁盘的空间。

  • 安装快: 由于依赖项共享和链接,使得 pnpm 在安装时候的速度比传统的包管理工具快

  • 支持monorepo: pnpm 支持在单个仓库中管理多个项目,共享依赖,更好的支持大型项目。

  • 版本锁定: pnpm 可以使用精确的版本锁定,避免了一些未知的依赖升级

可以通过以下命令下载 pnpm

npm i -g pnpm

以下是pnpm支持的node版本

Node版本pnpm5pnpm6pnpm7pnpm8
node.js 12
node.js 14
node.js 16
node.js 18

什么是 monorepo?

monorepo 指的就是多个项目在同一个仓库中管理,这些项目或模块是紧密关联的,共享代码,共享文档,统一的构建和部署

搭建 pnpm + monorepo

接下来就跟着我的步骤转生从零开始第一次搭建 monorepo

  1. 打好根基,首先能不能装X,能不能学好这门功法,你要使用这个命令查看你面板是否开启此功能

    npm i -g pnpm
    pnpm -v // 查看面板
    
  2. 创建一个目录,作为你学习 monorepo 发育的根据地

  3. 初始化 monorepo,进入你的根据地,使用心法秘籍第一个指令

    pnpm init -y
    
  4. 创建子项目,在你的根据地 monorepo 项目中,每个子项目也就是你的独立模块。在项目根据地下创建不同子项目来容纳不同的子项目,也就是修炼不同的功法,每个功法都是独立的。

    - 根据地
      - packages
        - package1
        - package2
      - package.json
    
  5. 在你的根目录下面安装依赖项目,每个子项目中也可以安装子项目的依赖

    pnpm i
    
  6. 设置 monorepo 配置,在你的根据地下面创建一个文件,该功法名为 pnpm-workspace.yaml ,用于配置 monorepo 的行为

    - 根据地
      - packages
        - package1
        - package2
      - pnpm-worksapce.yaml
      - package.json
    

    pnpm-workspace.yaml 中:

    packages:
      - packages/*
    
  7. 这里,我们需要不要心急,先暂留一部分,待你习得其他技能我们在一一让他完善

vite

什么是 vite

vite 是一个现代化前端构建工具,快,从我记得他的印象开始就是这个词来形容,他通过开发服务器使用原生ES 模块导入,在首次访问的时候只需要编译当前页面的代码即可。实现了 ES 模块热重载,在开发过程中可以看到实时效果等一系列功能 。

学会他,那么你的实力必上一层楼。

学会 vite 创建命令

如何拥有这门技能,我只偷偷告诉你一个人。首先想起最初我们根目录下面的文件是啥样,接下来我们要创建examples 文件夹现在我们的技能树是这样

- 根据地
  - examples
  - packages
    - package1
    - package2
  - pnpm-worksapce.yaml
  - package.json

pnpm-worksapce.yaml 中为这样

package:
  - 'packages/*'
  - 'examples/*'

接着我们就开始入门vite第一式,如果有些灵根比较弱的同学没有pnpm,请回炉重造,那么我们在 examples 中执行以下:

pnpm create vite

接着一就要开始召唤你的第一个宠物 为这个 project name 起名为 web,选择为 vue 、选择为 typescript,当然这里你可以不必按部就班

vitest

什么是vitest?

vitest 是一款单元测试工具,他与 vite 通用的配置,转换器,解析器,智能监听文件,就像HRM,兼容 Jestvitest 旨在将自己定位为 vite 项目的首选测试框架,即使对于不使用 vite 的项目也是一个可靠的替代方案

在项目中安装vitest

欲练此功,必先安装和配置以下,接下来就带各位如何修得此法,先看这个命令,在你的根目录下执行:

pnpm add -D vitest

然后根目录创建一个 vite.config.ts

import { defineConfig } from "vitest/config";
const include: string[] = [];
const testIncludes = [
  {
    dir: '"./packages/**/{__test__,test}/**/*.{ts,js}"', // 指的是 packages 下test或者__test__中所有的js、ts测试文件
  },
];
for (let index = 0; index < testIncludes.length; index++) {
  const element = testIncludes[index];
  include.push(element.dir);
}
export default defineConfig((config) => {
  return {
    test: {
      // 包含 packages下所有文件的 __test__ 文件夹下的所有文件
      include,
      // 排除 packages下所有文件的 node_modules 和 dist 文件夹
      exclude: ["./packages/**/{node_modules,dist}/**"],
    },
  };
});

package.json 中新增 test 命令

{
  "name": "sakana",
  "version": "1.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "vitest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "vitest": "^0.34.2"
  }
}

此事的项目结构

- 根据地
  - examples
  - packages
    - package1
    - package2
  - pnpm-worksapce.yaml
  - package.json
  - vite.config.ts

写一个简单的测试

语法简单熟悉:

  • describe : 描述, decribe会形成一个作用域
  • it/test : 定义了一组关于测试期望的方法,它接收测试名称和一个含有测试期望的函数
  • expect :用来创建断言
  • toBe :可用于断言基础对象是否相等

简单配置了一下,在 packages 中创建这样的一个目录 operate/src/indexoperate/test/sum.spec.ts

operate/test/sum.spec.ts 写下这些

import { describe, it, expect } from "vitest";
import { sum } from "../src";
describe("sum", () => {
  // sum 应该是一个函数
  it("sum should be a function", () => {
    expect(typeof sum).toBe("function");
  });
  // 接收两个参数返回相加结果
  it("sum should receive the sum of two parameters and return", () => {
    expect(sum(1, 2)).toBe(3);
  });
});

根据测试完成 sum 函数,在 operate/src/index

export const sum = (a: number, b: number) => {
  return a + b;
};

执行 pnpm test

这里我们先设计测试用例,然后在完成功能,这也称为 TDDTDD 不是一门技术,而是一种开发理念。它的核心就是开发人员在实现代码功能之前,先设计好测试用例,然后再根据测试用例的代码编写功能代码,最终让开发前设计的测试用例都能执行通过。

typescript

学习这里的时候,我们简单了解一下typescript这门秘籍,大概从一些常见类型,如何声明接口以及区别,typescript 条件判断,递归怎么写,然后实现一下简单的 类型体操

常用类型

类型例子描述
number1,-3任意数字
string'hi'任意字符串
booleantrue,false布尔值true or false
字面量其本身限制变量的值就是该字面量的值
any*任意类型
unknown*类型安全的any
void空值(undefined)没有值(或undefined)
never没有值不能是任何值
object{name:'zs'}任意的JS对象
array[1,2,3]任意JS数组
tuple[4,5]元素,TS新增类型,固定长度数组
enumenum[A,B]枚举,TS中新增类型

声明接口

在 typeScript 中,interfacetype 都用于声明自定义的数据类型,但它们有一些区别。

使用 interface

interface 用于定义对象的结构,它可以描述对象的属性、方法和类等。以下是使用 interface 声明的一个例子:

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

const person: Person = {
  firstName: "John",
  lastName: "Doe",
  age: 30,
};

我们定义了一个名为 Person 的接口,描述了一个拥有 firstNamelastNameage 属性的对象。然后,我们创建了一个符合 Person 接口定义的对象。

使用 type

type 用于创建自定义的类型别名,可以用于任何类型,不仅限于对象。以下是使用 type 声明的一个例子:

type Point = {
  x: number;
  y: number;
};

const point: Point = {
  x: 10,
  y: 20,
};

我们使用 type 定义了一个名为 Point 的类型别名,它描述了一个拥有 xy 属性的对象。与接口类似,我们使用 Point 类型别名创建了一个对象。

区别

  1. 扩展性: interface 具有扩展性,可以通过 extends 关键字扩展其他接口。而 type 别名不具备这种扩展性。

  2. 兼容性: interfacetype 在类型兼容性方面有些差异。type 别名可以进行更灵活的操作,而 interface 更倾向于严格的匹配。

  3. 可读性: interface 更适合用于描述对象结构,因为它更加直观,而 type 别名更适合用于定义复杂类型别名,或者联合类型。

  4. 类型重用: 如果你需要多次使用相同的结构定义,使用 interface 会更方便,因为可以多次引用同一个接口定义。而 type 别名相对更灵活。

在实际应用中,interfacetype 都有其用武之地。选择使用哪个取决于你的需求和个人偏好。

类型运算

条件运算

typescript 中 通过 extends ? : 来写判断 接下来看一段代码:

type IsNubmer<T> = T extends number ? true : false; 

type res1 = IsNumber<'1'>; // false

type res2 = IsNumber<2>; // true

T extends number ? true : false 主要看这一段代码 extends ? : 就是typescript中的 条件类型(conditional-types) ,它看起来和 javascript 中 条件 ? true表达式 : false表达式 有点像,当 extends 左边的类型可以赋值给右边的类型,你将返回 true ,否则你将获得最后一个分支 false

推导 infer

typescript 中通过 infer 提取,infer 用在条件类型中,表示待推断的类型变量,也就是说 infer 用在 extends 右侧,表示 extends 右侧的类型待推断,接下来看一段代码:

type FirstString<Str extends string> = Str extends `${infer First}${infer Rest}`
  ? First
  : never;

type FirstStringRes = FirstString<"123">; // 1

type FirstString<Str extends string> = ...,这一行代码定义了一个名为 FirstString 的类型别名,它接受一个泛型类型 Str,该泛型类型必须是一个字符串类型。这里 extends 是约束Strstring 类型 ${infer First}${infer Rest}, 这部分是一个模板字符串,它用于将输入字符串 Str 拆分为两部分:第一个字符 First 和剩余部分 Rest。这里使用了 TypeScript 的模板字符串中的 ${} 语法,其中 infer 是一个 TypeScript 类型推断的关键字。 ? First : never,这是一个条件表达式,它判断模板字符串是否成功匹配。如果成功匹配(即输入字符串 Str 的结构是 ${infer First}${infer Rest}),则返回第一个字符 First 的类型;否则返回 never 类型。

映射类型

先学会 keyof 操作符,简单这一段代码:

type Point = { x: number; y: number };
type P = keyof Point; // x | y

keyof 将对象类型生成其键的字符串或者数字字面量联合类型,最终就是 x | y

实现一个映射类型

type MapType<T> = {
  [Key in keyof T]: T[Key];
};

type res3 = MapType<{ a: 1; b: 2 }>;
// keyof T 是查询索引类型中所有的索引,叫做索引查询
// T[Key] 是取索引类型某个索引的值,叫做索引访问

递归

typescript 中不支持循环,但是支持递归,通常可以递归自调用处理类型,直到满足结束条件,就完成了,不确定数量的类型编程,达到循环效果,这里可以用做了解,接下来看一段代码,字符串小写转为大写:

type ToUpperCase<S extends string> = S extends `${infer First}${infer Rest}`
  ? `${Uppercase<First>}${ToUpperCase<Rest>}`
  : S;

type ToUpperCaseRes = ToUpperCase<"sakana">; // SAKANA

Uppercase 是typescript中内置工具,转为大写,${infer First}${infer Rest} 这里我们提取出 FirstRest ,在 ${Uppercase<First>}${ToUpperCase<Rest>} 中 通过 ToUpperCase<Rest> 递归调用,直到结束

一些类型体操

  • typescript 实现 Push 末尾添加一位

    先看JavaScript如何实现:

    function Push(arr, item) {
      return [...arr, item];
    }
    

    typescript 实现如下:

    type Push<Arr extends unknown[], T> = Arr extends [] ? [T] : [...Arr, T];
    
    type PushRes = PushArr<[1, 2, 3], 4>;
    
  • typescript 实现 DeepReadonly 转为可读

    type DeepReadonly<T extends Record<string, any>> = T extends any
      ? {
          readonly [Key in keyof T]: T[Key] extends object
            ? T[Key] extends Function
              ? T[Key]
              : DeepReadonly<T[Key]>
            : T[Key];
        }
      : never;
    
    type DeepReadonlyRes = DeepReadonly<{
      name: string;
      age: number;
      friend: {
        eat: () => void;
        name: string;
        age: number;
      };
    }>;
    
    

推荐链接

发布订阅

发布订阅是一种设计模式,用于在软件组件之间建立一种松散耦合的通信机制。在发布订阅模式中,一个组件(发布者)发布事件,而其他组件(订阅者)监听这些事件并做出响应。

优点

  • **松散耦合:**发布订阅模式可以让组件之间实现松散耦合,发布者和订阅者不需要直接了解对方的存在。
  • 可扩展性: 新的订阅者可以轻松添加到现有的发布者中,不会影响其他组件。
  • 解耦逻辑: 发布者和订阅者之间的逻辑是解耦的,一个组件的改变不会直接影响其他组件。

缺点

  • 难以调试: 如果订阅者很多,发布事件时可能会难以追踪哪些订阅者会被调用。
  • 潜在内存泄漏: 必须手动取消订阅,否则可能导致内存泄漏。
  • 顺序不确定性: 发布订阅模式中的订阅者响应的顺序是不确定的,这可能导致一些问题。

了解他们的优缺点,结合自己的编程经验,你可以总结出什么时候用,什么时候不用,修炼这些技能的时候,还是要切合实际场景,打出对应的组合拳!

实现mitt

mittvue3 项目中跨组件通信的一种技术方案,通过学习百行代码,简单复现基本功能,把之前的知识点串联起来,接下来我们通过先写 单元测试 然后逐步实现 mitt 功能,接下来各位准备好,我们开始

准备动手

创建这些目录 packages/mitt/__test__/index.spec.ts,先写一个简单的

import { test, vi, describe, it, expect, beforeEach } from "vitest";
import { mitt } from "../../src";

describe("mitt", () => {
  // 应该是一个函数
  it("should be a function", () => {
    expect(typeof mitt).toBe("function");
  });
});

创建 packages/mitt/src/index.ts ,根据单测描述应该是一个函数所以我们需要干嘛,写一个函数,然后导出 mitt ,如下:

export function mitt() {}

写完之后,执行 pnpm test 看看是否通过

传入一个参数

packages/mitt/__test__/index.spec.ts 写单元测试,mitt 应该传入一个map 并且返回的这个参数

describe("mitt", () => {
  // 传入一个可选的map,返回这个map
  it("should accept an optional event handler map", () => {
    const event = new Map();
    const emitter = mitt(event);
    expect(emitter.events).toBe(event);
  });
});

回到 packages/mitt/src/index.ts 完成功能,这里会简单设计一下 type ,如果你确保你实操过之前的typescript,那么这里对你来说非常轻松

export type EventType = string | symbol;
export type HandlersMap<Events extends Record<EventType, unknown>> = Map<
  keyof Events,
  any
>;                        
export type Emitter<Events extends Record<EventType, unknown>> = {
  events: HandlersMap<Events>;
};

根据我们的单元测试完成传入一个参数,返回 events

export function mitt<Events extends Record<EventType, unknown>>(
  all?: HandlersMap<Events>
): Emitter<Events> {
  const events = all || new Map();

  return {
    events,
  };
}

执行 pnpm test 通过即可

实现 on

packages/mitt/__test__/index.spec.ts 中,描述 on 要做什么事

  • on 应该是一个函数
  • 注册新的事件处理程序
  • 可以是任意字符串作为type,比如接收 'constructor'
  • 可以追加一个handler
  • 不区分大小写
  • type可以接收symbol
  • 可以重复监听
import { test, vi, describe, it, expect, beforeEach } from "vitest";
import { HandlersMap, Emitter, mitt } from "../../src";

describe("mitt#", () => {
  const evetType = Symbol("evetType");
  type Events = {
    foo: unknown;
    constructor: unknown;
    FOO: unknown;
    bar: unknown;
    Bar: unknown;
    "baz:bat!": unknown;
    "baz:baT!": unknown;
    Foo: unknown;
    [evetType]: unknown;
  };

  let events: HandlersMap<Events>, inst: Emitter<Events>;

  beforeEach(() => {
    events = new Map();
    inst = mitt(events);
  });

  describe("on", () => {
    it("should be a function ", () => {
      expect(typeof inst.on).toBe("function");
    });

    it("should register event handler for new type", () => {
      const foo = () => {};
      inst.on("foo", foo);
      expect(events.get("foo")).toEqual([foo]);
    });

    it("should register handlers for any type strings", () => {
      const foo = () => {};
      inst.on("constructor", foo);
      expect(events.get("constructor")).toEqual([foo]);
    });

    it("should append hanler for existing type", () => {
      const foo = () => {};
      const bar = () => {};
      inst.on("foo", foo);
      inst.on("foo", bar);
      expect(events.get("foo")).toEqual([foo, bar]);
    });

    it("should NOT normalize case", () => {
      const foo = () => {};
      inst.on("FOO", foo);
      inst.on("Bar", foo);
      inst.on("baz:bat!", foo);

      expect(events.get("FOO")).toEqual([foo]);
      expect(events.has("foo")).toEqual(false);
      expect(events.get("Bar")).toEqual([foo]);
      expect(events.has("bar")).toEqual(false);
      expect(events.get("baz:bat!")).toEqual([foo]);
    });

    it("can take symbol for event tpye", () => {
      const foo = () => {};
      inst.on(evetType, foo);
      expect(events.get(evetType)).toEqual([foo]);
    });

    it("should add duplicate listeners", () => {
      const foo = () => {};
      inst.on("foo", foo);
      inst.on("foo", foo);
      expect(events.get("foo")).toEqual([foo, foo]);
    });
  });	
});

回到 packages/mitt/src/index.ts 完成功能,这里我们会接收一个handler,先完善typescript类型,然后再实现 on

export type EventType = string | symbol;
export type Handler<T = unknown> = (event: T) => void;
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
export type HandlersMap<Events extends Record<EventType, unknown>> = Map<
  keyof Events,
  EventHandlerList<Events[keyof Events]>
>;
export type Emitter<Events extends Record<EventType, unknown>> = {
  events: HandlersMap<Events>;
  emit?<Key extends keyof Events>(type: Key, event?: Events[Key]): void;
  on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
  off?<Key extends keyof Events>(
    type: Key,
    handler?: Handler<Events[Key]>
  ): void;
};

实现 on

export function mitt<Events extends Record<EventType, unknown>>(
  all?: HandlersMap<Events>
): Emitter<Events> {
  const events = all || new Map();
  
  function on<Key extends keyof Events>(
    type: Key,
    handler: Handler<Events[Key]>
  ) {
    const handles = events.get(type) as EventHandlerList<Events[Key]>;
    const added = handles && handles.push(handler);
    if (!added) {
      events.set(type, [handler]);
    }
  }
  return {
    events,
    on,
  };
}

执行pnpm test

实现 off

packages/mitt/__test__/index.spec.ts 中,描述 off 要做什么事

  • off 是一个函数
  • 移除对应 type
  • 不应
  describe("off", () => {
    it("should be a function", () => {
      expect(typeof inst.off).toBe("function");
    });

    it("should remove for type", () => {
      const foo = () => {};
      inst.on("foo", foo);
      inst.off("foo", foo);
      expect(events.get("foo")).toEqual([]);
    });

    it("should NOT normalize case", () => {
      const foo = () => {};
      inst.on("FOO", foo);
      inst.off("FOO", foo);
      expect(events.get("FOO")).toEqual([]);
      expect(events.has("foo")).toEqual(false);
    });

    it("should remove only the listener passed", () => {
      const foo = () => {};
      const bar = () => {};
      inst.on("foo", foo);
      inst.on("foo", bar);
      inst.off("foo", foo);
      expect(events.get("foo")).toEqual([bar]);
    });
  });

回到 packages/mitt/src/index.ts 完成 off

export function mitt<Events extends Record<EventType, unknown>>(
  all?: HandlersMap<Events>
): Emitter<Events> {
  const events = all || new Map();

  function on<Key extends keyof Events>(
    type: Key,
    handler: Handler<Events[Key]>
  ) {
    const handles = events.get(type) as EventHandlerList<Events[Key]>;
    const added = handles && handles.push(handler);
    if (!added) {
      events.set(type, [handler]);
    }
  }
    
  function off<Key extends keyof Events>(
    type: Key,
    handler?: Handler<Events[Key]>
  ) {
    const handlers = events.get(type) as EventHandlerList<Events[Key]>;
    if (handlers) {
      if (handler) {
        // 删除事件处理程序 >>> 0 保证是正整数
        handlers.splice(handlers.indexOf(handler) >>> 0, 1);
      } else {
        events.set(type, []);
      }
    }
  }

  return {
    events,
    on,
    off,
  };
}

执行 pnpm test

实现 emit

packages/mitt/__test__/index.spec.ts 中,描述 emit 要做什么事

  • 是一个函数
  • 调用 type 处理程序
  • 不区分大小写,foo 至少被调用一次
  describe("emit", () => {
    it("should be a function", () => {
      expect(typeof inst.emit).toBe("function");
    });

    it("should invoke handlers for type", () => {
      const event = { type: "foo" };
      inst.on("foo", (one: any, two?: unknown) => {
        expect(one).toBe(event);
        expect(two).toBe(undefined);
      });

      inst.emit("foo", event);
    });

    it("should NOT normalize case", () => {
      const foo = vi.fn();
      inst.on("FOO", foo);
      inst.emit("FOO");
      expect(foo).toHaveBeenCalledTimes(1);

      const event = { type: "FOO" };
      inst.on("FOO", (one: any, two?: unknown) => {
        expect(one).toBe(event);
        expect(two).toBe(undefined);
      });

      inst.emit("FOO", event);
    });
  });

回到 packages/mitt/src/index.ts 完成 emit

export function mitt<Events extends Record<EventType, unknown>>(
  all?: HandlersMap<Events>
): Emitter<Events> {
  const events = all || new Map();

  function on<Key extends keyof Events>(
    type: Key,
    handler: Handler<Events[Key]>
  ) {
    const handles = events.get(type) as EventHandlerList<Events[Key]>;
    const added = handles && handles.push(handler);
    if (!added) {
      events.set(type, [handler]);
    }
  }
  function off<Key extends keyof Events>(
    type: Key,
    handler?: Handler<Events[Key]>
  ) {
    const handlers = events.get(type) as EventHandlerList<Events[Key]>;
    if (handlers) {
      if (handler) {
        // 删除事件处理程序 >>> 0 保证是正整数
        handlers.splice(handlers.indexOf(handler) >>> 0, 1);
      } else {
        events.set(type, []);
      }
    }
  }

  function emit<Key extends keyof Events>(type: Key, event?: Events[Key]) {
    // 调用事件处理程序
    const handlers = events.get(type) as EventHandlerList<Events[Key]>;
    if (handlers) {
      handlers.forEach((fn: Handler<Events[Key]>) => {
        fn(event!);
      });
    }
  }

  return {
    events,
    on,
    off,
    emit,
  };
}

执行 pnpm test

这里我们就实现了,然后我们可以在 examples/web 下安装试试对了,mitt 记得初始化 package.json 然后安装对应的名称就好了,不明白可以看我仓库

总结

引用一句话:无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑、毁掉你本就不多的热情和定力

地址: 戳这里

往期回顾