实现百行不到的 mitt 我学到了这些技术
前言
本章是实现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版本 | pnpm5 | pnpm6 | pnpm7 | pnpm8 |
---|---|---|---|---|
node.js 12 | ✔ | ✔ | ❌ | ❌ |
node.js 14 | ✔ | ✔ | ✔ | ❌ |
node.js 16 | ❓ | ✔ | ✔ | ✔ |
node.js 18 | ❓ | ✔ | ✔ | ✔ |
什么是 monorepo?
monorepo 指的就是多个项目在同一个仓库中管理,这些项目或模块是紧密关联的,共享代码,共享文档,统一的构建和部署
搭建 pnpm + monorepo
接下来就跟着我的步骤转生从零开始第一次搭建 monorepo。
-
打好根基,首先能不能装X,能不能学好这门功法,你要使用这个命令查看你面板是否开启此功能
npm i -g pnpm pnpm -v // 查看面板
-
创建一个目录,作为你学习 monorepo 发育的根据地
-
初始化 monorepo,进入你的根据地,使用心法秘籍第一个指令
pnpm init -y
-
创建子项目,在你的根据地 monorepo 项目中,每个子项目也就是你的独立模块。在项目根据地下创建不同子项目来容纳不同的子项目,也就是修炼不同的功法,每个功法都是独立的。
- 根据地 - packages - package1 - package2 - package.json
-
在你的根目录下面安装依赖项目,每个子项目中也可以安装子项目的依赖
pnpm i
-
设置 monorepo 配置,在你的根据地下面创建一个文件,该功法名为
pnpm-workspace.yaml
,用于配置 monorepo 的行为- 根据地 - packages - package1 - package2 - pnpm-worksapce.yaml - package.json
在
pnpm-workspace.yaml
中:packages: - packages/*
-
这里,我们需要不要心急,先暂留一部分,待你习得其他技能我们在一一让他完善
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,兼容 Jest。vitest
旨在将自己定位为 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/index
和 operate/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
这里我们先设计测试用例,然后在完成功能,这也称为 TDD
,TDD
不是一门技术,而是一种开发理念。它的核心就是开发人员在实现代码功能之前,先设计好测试用例,然后再根据测试用例的代码编写功能代码,最终让开发前设计的测试用例都能执行通过。
typescript
学习这里的时候,我们简单了解一下typescript这门秘籍,大概从一些常见类型,如何声明接口以及区别,typescript 条件判断,递归怎么写,然后实现一下简单的 类型体操
常用类型
类型 | 例子 | 描述 |
---|---|---|
number | 1,-3 | 任意数字 |
string | 'hi' | 任意字符串 |
boolean | true,false | 布尔值true or false |
字面量 | 其本身 | 限制变量的值就是该字面量的值 |
any | * | 任意类型 |
unknown | * | 类型安全的any |
void | 空值(undefined) | 没有值(或undefined) |
never | 没有值 | 不能是任何值 |
object | {name:'zs'} | 任意的JS对象 |
array | [1,2,3] | 任意JS数组 |
tuple | [4,5] | 元素,TS新增类型,固定长度数组 |
enum | enum[A,B] | 枚举,TS中新增类型 |
声明接口
在 typeScript 中,interface
和 type
都用于声明自定义的数据类型,但它们有一些区别。
使用 interface
interface
用于定义对象的结构,它可以描述对象的属性、方法和类等。以下是使用 interface
声明的一个例子:
interface Person {
firstName: string;
lastName: string;
age: number;
}
const person: Person = {
firstName: "John",
lastName: "Doe",
age: 30,
};
我们定义了一个名为 Person
的接口,描述了一个拥有 firstName
、lastName
和 age
属性的对象。然后,我们创建了一个符合 Person
接口定义的对象。
使用 type
type
用于创建自定义的类型别名,可以用于任何类型,不仅限于对象。以下是使用 type
声明的一个例子:
type Point = {
x: number;
y: number;
};
const point: Point = {
x: 10,
y: 20,
};
我们使用 type
定义了一个名为 Point
的类型别名,它描述了一个拥有 x
和 y
属性的对象。与接口类似,我们使用 Point
类型别名创建了一个对象。
区别
-
扩展性:
interface
具有扩展性,可以通过extends
关键字扩展其他接口。而type
别名不具备这种扩展性。 -
兼容性:
interface
和type
在类型兼容性方面有些差异。type
别名可以进行更灵活的操作,而interface
更倾向于严格的匹配。 -
可读性:
interface
更适合用于描述对象结构,因为它更加直观,而type
别名更适合用于定义复杂类型别名,或者联合类型。 -
类型重用: 如果你需要多次使用相同的结构定义,使用
interface
会更方便,因为可以多次引用同一个接口定义。而type
别名相对更灵活。
在实际应用中,interface
和 type
都有其用武之地。选择使用哪个取决于你的需求和个人偏好。
类型运算
条件运算
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
是约束Str
为 string
类型
${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}
这里我们提取出 First
和 Rest
,在 ${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
mitt
是 vue3
项目中跨组件通信的一种技术方案,通过学习百行代码,简单复现基本功能,把之前的知识点串联起来,接下来我们通过先写 单元测试 然后逐步实现 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
然后安装对应的名称就好了,不明白可以看我仓库
总结
引用一句话:无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑、毁掉你本就不多的热情和定力
地址: 戳这里
往期回顾
转载自:https://juejin.cn/post/7270321740266766372