阅读视图
美国FCC:SpaceX申请部署百万颗卫星 欲建轨道AI数据中心网络
东莞:2025年地区生产总值12760.2亿元 同比增长4%
CSS伪元素:给HTML穿上"隐形斗篷"的魔法
柯睿辰将接替高翔出任宝马集团大中华区总裁兼首席执行官
uni-appD4(uni-forms学习与回顾)
vue视频播放器:基于vue-video-player的自定义视频播放器实现
什么是"阻塞渲染"?如何避免 JavaScript 代码阻塞页面渲染?
在 HTML 中引入 JavaScript 有哪几种方式?它们各自的优缺点是什么?
欣旺达再递表 冲刺港股上市
雷军:将于2月1日晚8点在北京小米汽车工厂实验室直播
2025年全国市场监管部门共查办广告违法案件44521件,罚没金额2.52亿
可灵AI推出全新3.0系列模型
type-challenges(ts类型体操): 15 - 最后一个元素
15 - 最后一个元素
by Anthony Fu (@antfu) #中等 #array
题目
在此挑战中建议使用TypeScript 4.0
实现一个Last<T>泛型,它接受一个数组T并返回其最后一个元素的类型。
例如
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type tail1 = Last<arr1> // 应推导出 'c'
type tail2 = Last<arr2> // 应推导出 1
在 Github 上查看:tsch.js.org/15/zh-CN
代码
/* _____________ 你的代码 _____________ */
type Last<T extends any[]> = T extends [...infer _, infer R] ? R : never
关键解释:
-
T extends [...infer _, infer R]:通过infer提取数组的最后一个元素R。 -
? R : never:如果数组非空,返回R;否则返回never。
相关知识点
extends
| 使用维度 | 核心作用 | 示例场景 |
|---|---|---|
| 类型维度 | 做类型约束或条件判断(类型编程核心) | 限定泛型范围、判断类型是否兼容、提取类型片段 |
| 语法维度 | 做继承(复用已有结构) | 接口继承、类继承 |
extends 做类型约束或条件判断
- 泛型约束:限定泛型的取值范围
// 约束 T 必须是「拥有 length 属性」的类型(比如 string/数组)
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// 合法调用(符合约束)
getLength("hello"); // ✅ string 有 length,返回 5
getLength([1, 2, 3]); // ✅ 数组有 length,返回 3
// 非法调用(超出约束)
getLength(123); // ❌ 报错:number 没有 length 属性
- 条件类型:类型版 三元运算符
// 基础示例:判断类型是否为字符串
type IsString<T> = T extends string ? true : false;
type A = IsString<"test">; // true(符合)
type B = IsString<123>; // false(不符合)
分布式条件类型(联合类型专用): 当 T 是联合类型时,extends 会自动拆分联合类型的每个成员,逐个判断后再合并结果。
type Union = string | number | boolean;
// 拆分逻辑:string→string,number→never,boolean→never → 合并为 string
type OnlyString<T> = T extends string ? T : never;
type Result = OnlyString<Union>; // Result = string
注意:只有泛型参数是 裸类型(没有被 []/{} 包裹)时,才会触发分布式判断:
// 包裹后不触发分布式,整体判断 [string|number] 是否兼容 [string]
type NoDist<T> = [T] extends [string] ? T : never;
type Result2 = NoDist<Union>; // never(整体不兼容)
- 配合
infer:提取类型片段(黄金组合)
// 提取 Promise 的返回值类型
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type C = UnwrapPromise<Promise<string>>; // string(提取成功)
type D = UnwrapPromise<number>; // number(不满足条件,返回原类型)
extends 做继承(复用已有结构)
- 接口继承:复用 + 扩展属性
// 基础接口
interface User {
id: number;
name: string;
}
// 继承 User,并扩展新属性
interface Admin extends User {
role: "admin" | "super_admin"; // 新增权限属性
}
// 必须包含继承的 + 扩展的所有属性
const admin: Admin = {
id: 1,
name: "张三",
role: "admin"
};
// 多接口继承
interface HasAge { age: number; }
interface Student extends User, HasAge {
className: string; // 同时继承 User + HasAge
}
- 类继承:复用父类的属性 / 方法
class Parent {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi() {
console.log(`Hi, ${this.name}`);
}
}
// 继承 Parent 类
class Child extends Parent {
age: number;
constructor(name: string, age: number) {
super(name); // 必须调用父类构造函数(初始化父类属性)
this.age = age;
}
// 重写父类方法
sayHi() {
super.sayHi(); // 调用父类原方法
console.log(`I'm ${this.age} years old`);
}
}
const child = new Child("李四", 10);
child.sayHi(); // 输出:Hi, 李四 → I'm 10 years old
补充:类实现接口用 implements(不是 extends)
// 定义接口(契约:规定必须有 id、name 属性,以及 greet 方法)
interface Person {
id: number;
name: string;
greet(): void; // 仅定义方法签名,无实现
}
// 类实现接口(必须严格遵守契约)
class Employee implements Person {
// 必须实现接口的所有属性
id: number;
name: string;
// 构造函数初始化属性
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
// 必须实现接口的 greet 方法(具体实现由类自己定义)
greet() {
console.log(`Hi, I'm ${this.name}, ID: ${this.id}`);
}
}
// 实例化使用
const emp = new Employee(1, "张三");
emp.greet(); // 输出:Hi, I'm 张三, ID: 1
// 接口1:基础信息
interface Identifiable {
id: number;
getId(): number;
}
// 接口2:可打印
interface Printable {
printInfo(): void;
}
// 类同时实现两个接口(必须实现所有接口的成员)
class Product implements Identifiable, Printable {
id: number;
name: string; // 类可扩展接口外的属性
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
// 实现 Identifiable 的方法
getId(): number {
return this.id;
}
// 实现 Printable 的方法
printInfo() {
console.log(`Product: ${this.name}, ID: ${this.getId()}`);
}
}
const product = new Product(100, "手机");
console.log(product.getId()); // 100
product.printInfo(); // Product: 手机, ID: 100
infer
infer 是 TypeScript 在条件类型中提供的关键字,用于声明一个 待推导的类型变量(类似给类型起一个临时名字),只能在 extends 子句中使用。它的核心作用是:从已有类型中提取 / 推导我们需要的部分,而无需手动硬编码类型。
infer 必须配合条件类型使用,语法结构如下:
// 基础结构:推导 T 的类型为 U,若能推导则返回 U,否则返回 never
type InferType<T> = T extends infer U ? U : never;
type Example = InferType<string>; // Example 类型为 string
type Example2 = InferType<number[]>; // Example2 类型为 number[]
高频使用场景:
1. 提取函数的返回值类型
// 定义类型工具:提取函数的返回值类型
type GetReturnType<Fn> = Fn extends (...args: any[]) => infer R ? R : never;
// 测试用函数
const add = (a: number, b: number): number => a + b;
const getUser = () => ({ name: "张三", age: 20 });
// 使用类型工具
type AddReturn = GetReturnType<typeof add>; // AddReturn 类型为 number
type UserReturn = GetReturnType<typeof getUser>; // UserReturn 类型为 { name: string; age: number }
2. 提取数组的元素类型
// 定义类型工具:提取数组元素类型
type GetArrayItem<T> = T extends (infer Item)[] ? Item : never;
// 测试
type NumberArray = GetArrayItem<number[]>; // NumberArray 类型为 number
type StringArray = GetArrayItem<string[]>; // StringArray 类型为 string
type MixedArray = GetArrayItem<[string, number]>; // MixedArray 类型为 string | number
3. 提取 Promise 的泛型参数类型
// 定义类型工具:提取 Promise 的泛型类型
type GetPromiseValue<T> = T extends Promise<infer Value> ? Value : never;
// 测试
type PromiseString = GetPromiseValue<Promise<string>>; // PromiseString 类型为 string
type PromiseUser = GetPromiseValue<Promise<{ id: number }>>; // PromiseUser 类型为 { id: number }
4. 提取函数的参数类型
// 定义类型工具:提取函数参数类型
type GetFunctionParams<Fn> = Fn extends (...args: infer Params) => any ? Params : never;
// 测试
const fn = (name: string, age: number): void => {};
type FnParams = GetFunctionParams<typeof fn>; // FnParams 类型为 [string, number]
// 进一步:提取第一个参数的类型
type FirstParam = GetFunctionParams<typeof fn>[0]; // FirstParam 类型为 string
never
never 表示永不存在的类型:
- 没有任何类型能赋值给
never(除了never自身); -
never可以赋值给任意类型(因为它是所有类型的子类型); - 不会有任何实际值属于
never类型。
let n: never;
let num: number = 123;
let u: unknown = "hello";
let v: void = undefined;
// 1. 任何类型都不能赋值给 never(除了自身)
n = num; // ❌ 报错:number 不能赋值给 never
n = u; // ❌ 报错:unknown 不能赋值给 never
n = v; // ❌ 报错:void 不能赋值给 never
n = undefined; // ❌ 报错:undefined 也不行
n = n; // ✅ 仅自身可赋值
// 2. never 可以赋值给任意类型
num = n; // ✅ 正常
u = n; // ✅ 正常
v = n; // ✅ 正常
- 泛型的边界约束: 通过泛型约束让不满足条件的泛型类型变为
never,从而达到限制类型范围的目的。
// 定义泛型:仅允许 T 为 string 类型,否则 T 为 never
type OnlyString<T> = T extends string ? T : never;
// 满足条件:T 为 string,结果正常
type Str1 = OnlyString<"hello">; // Str1 = "hello"
type Str2 = OnlyString<string>; // Str2 = string
// 不满足条件:T 为非 string,结果为 never
type Num = OnlyString<number>; // Num = never
type Bool = OnlyString<boolean>; // Bool = never
type Unk = OnlyString<unknown>; // Unk = never
// 实际使用:强制函数参数只能是 string 类型
function printStr<T>(val: OnlyString<T>) {
console.log(val);
}
printStr("hello"); // ✅ 正常
printStr(123); // ❌ 报错:number 不能赋值给 never
测试用例
/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<Last<[]>, never>>,
Expect<Equal<Last<[2]>, 2>>,
Expect<Equal<Last<[3, 2, 1]>, 1>>,
Expect<Equal<Last<[() => 123, { a: string }]>, { a: string }>>,
]
相关链接
分享你的解答:tsch.js.org/15/answer/z… 查看解答:tsch.js.org/15/solution… 更多题目:tsch.js.org/zh-CN
下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。
实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。
type-challenges(ts类型体操): 14 - 第一个元素
14 - 第一个元素
by Anthony Fu (@antfu) #简单 #array
题目
实现一个First<T>泛型,它接受一个数组T并返回它的第一个元素的类型。
例如:
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // 应推导出 'a'
type head2 = First<arr2> // 应推导出 3
在 Github 上查看:tsch.js.org/14/zh-CN
代码
/* _____________ 你的代码 _____________ */
type First<T extends unknown[]> = T extends [] ? never : T[0]
关键解释:
-
T extends unknown[]用于约束T必须是一个数组类型。 -
T extends []用于判断数组是否为空。 -
T[0]用于获取数组的第一个元素。 -
never用于表示空数组的情况。
相关知识点
extends
| 使用维度 | 核心作用 | 示例场景 |
|---|---|---|
| 类型维度 | 做类型约束或条件判断(类型编程核心) | 限定泛型范围、判断类型是否兼容、提取类型片段 |
| 语法维度 | 做继承(复用已有结构) | 接口继承、类继承 |
extends 做类型约束或条件判断
- 泛型约束:限定泛型的取值范围
// 约束 T 必须是「拥有 length 属性」的类型(比如 string/数组)
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// 合法调用(符合约束)
getLength("hello"); // ✅ string 有 length,返回 5
getLength([1, 2, 3]); // ✅ 数组有 length,返回 3
// 非法调用(超出约束)
getLength(123); // ❌ 报错:number 没有 length 属性
- 条件类型:类型版 三元运算符
// 基础示例:判断类型是否为字符串
type IsString<T> = T extends string ? true : false;
type A = IsString<"test">; // true(符合)
type B = IsString<123>; // false(不符合)
分布式条件类型(联合类型专用): 当 T 是联合类型时,extends 会自动拆分联合类型的每个成员,逐个判断后再合并结果。
type Union = string | number | boolean;
// 拆分逻辑:string→string,number→never,boolean→never → 合并为 string
type OnlyString<T> = T extends string ? T : never;
type Result = OnlyString<Union>; // Result = string
注意:只有泛型参数是 裸类型(没有被 []/{} 包裹)时,才会触发分布式判断:
// 包裹后不触发分布式,整体判断 [string|number] 是否兼容 [string]
type NoDist<T> = [T] extends [string] ? T : never;
type Result2 = NoDist<Union>; // never(整体不兼容)
- 配合
infer:提取类型片段(黄金组合)
// 提取 Promise 的返回值类型
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type C = UnwrapPromise<Promise<string>>; // string(提取成功)
type D = UnwrapPromise<number>; // number(不满足条件,返回原类型)
extends 做继承(复用已有结构)
- 接口继承:复用 + 扩展属性
// 基础接口
interface User {
id: number;
name: string;
}
// 继承 User,并扩展新属性
interface Admin extends User {
role: "admin" | "super_admin"; // 新增权限属性
}
// 必须包含继承的 + 扩展的所有属性
const admin: Admin = {
id: 1,
name: "张三",
role: "admin"
};
// 多接口继承
interface HasAge { age: number; }
interface Student extends User, HasAge {
className: string; // 同时继承 User + HasAge
}
- 类继承:复用父类的属性 / 方法
class Parent {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi() {
console.log(`Hi, ${this.name}`);
}
}
// 继承 Parent 类
class Child extends Parent {
age: number;
constructor(name: string, age: number) {
super(name); // 必须调用父类构造函数(初始化父类属性)
this.age = age;
}
// 重写父类方法
sayHi() {
super.sayHi(); // 调用父类原方法
console.log(`I'm ${this.age} years old`);
}
}
const child = new Child("李四", 10);
child.sayHi(); // 输出:Hi, 李四 → I'm 10 years old
补充:类实现接口用 implements(不是 extends)
// 定义接口(契约:规定必须有 id、name 属性,以及 greet 方法)
interface Person {
id: number;
name: string;
greet(): void; // 仅定义方法签名,无实现
}
// 类实现接口(必须严格遵守契约)
class Employee implements Person {
// 必须实现接口的所有属性
id: number;
name: string;
// 构造函数初始化属性
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
// 必须实现接口的 greet 方法(具体实现由类自己定义)
greet() {
console.log(`Hi, I'm ${this.name}, ID: ${this.id}`);
}
}
// 实例化使用
const emp = new Employee(1, "张三");
emp.greet(); // 输出:Hi, I'm 张三, ID: 1
// 接口1:基础信息
interface Identifiable {
id: number;
getId(): number;
}
// 接口2:可打印
interface Printable {
printInfo(): void;
}
// 类同时实现两个接口(必须实现所有接口的成员)
class Product implements Identifiable, Printable {
id: number;
name: string; // 类可扩展接口外的属性
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
// 实现 Identifiable 的方法
getId(): number {
return this.id;
}
// 实现 Printable 的方法
printInfo() {
console.log(`Product: ${this.name}, ID: ${this.getId()}`);
}
}
const product = new Product(100, "手机");
console.log(product.getId()); // 100
product.printInfo(); // Product: 手机, ID: 100
unknown
作用是替代 any 处理 类型未知 的场景,同时保证类型检查的安全性。
-
所有类型(基本类型、对象、函数、数组等)都可以赋值给
unknown类型的变量; -
但
unknown类型的变量不能随意赋值给其他类型(仅能赋值给unknown和any); -
也不能直接操作
unknown类型的变量(比如调用方法、访问属性、做算术运算),必须先通过类型收窄确定其具体类型,这是它比any安全的关键。 -
与
any的区别
any 是任意类型,会关闭 TypeScript 的类型检查,而 unknown 是未知类型,保留类型检查,仅允许在确定类型后操作。两者的规则对比如下:
| 规则 | unknown |
any |
|---|---|---|
| 所有类型可赋值给它 | ✅ 支持 | ✅ 支持 |
| 它可赋值给其他类型 | ❌ 仅能赋值给 unknown/any
|
✅ 可赋值给任意类型(无限制) |
| 直接操作变量(调用方法 / 访问属性) | ❌ 不允许(必须类型收窄) | ✅ 允许(关闭类型检查) |
// 1. 所有类型都能赋值给 unknown/any
let u: unknown = 123;
u = "hello";
u = [1,2,3];
let a: any = 123;
a = "hello";
a = [1,2,3];
// 2. unknown 仅能赋值给 unknown/any(赋值给其他类型报错)
let num: number = u; // ❌ 报错:Type 'unknown' is not assignable to type 'number'
let u2: unknown = u; // ✅ 正常
let a2: any = u; // ✅ 正常
// any 可赋值给任意类型(无报错,即使类型不匹配)
let num2: number = a; // ✅ 无报错(但运行时可能出问题,类型不安全)
// 3. 直接操作 unknown 报错,操作 any 无限制
u.toFixed(); // ❌ 报错:Object is of type 'unknown'
a.toFixed(); // ✅ 无报错(即使 a 可能是字符串,TS 不检查)
- 类型收窄
2.1 typeof检查(适用于基本类型:number/string/boolean/undefined/null/symbol/bigint)
function handleUnknown(val: unknown) {
// 先通过 typeof 收窄为数字类型
if (typeof val === "number") {
console.log(val.toFixed(2)); // ✅ 正常:val 已确定是 number
}
// 收窄为字符串类型
else if (typeof val === "string") {
console.log(val.toUpperCase()); // ✅ 正常:val 已确定是 string
}
// 收窄为布尔类型
else if (typeof val === "boolean") {
console.log(val ? "真" : "假"); // ✅ 正常:val 已确定是 boolean
}
}
handleUnknown(123.456); // 输出 123.46
handleUnknown("hello"); // 输出 HELLO
handleUnknown(true); // 输出 真
2.2 instanceof检查(适用于引用类型:数组 / 类实例 / RegExp/Date 等)
function handleUnknown2(val: unknown) {
// 收窄为数组类型
if (val instanceof Array) {
console.log(val.push(4)); // ✅ 正常:val 已确定是 Array
}
// 收窄为 Date 类型
else if (val instanceof Date) {
console.log(val.toLocaleString()); // ✅ 正常:val 已确定是 Date
}
}
handleUnknown2([1,2,3]); // 输出 4(数组长度)
handleUnknown2(new Date()); // 输出当前时间字符串
2.3 类型断言
let u: unknown = "这是一个字符串";
// 断言为 string 类型后操作
let str = u as string;
console.log(str.length); // ✅ 正常:输出 7
// 错误断言(运行时报错)
let num = u as number;
console.log(num.toFixed()); // ❌ 运行时报错:num.toFixed is not a function
never
never 表示永不存在的类型:
- 没有任何类型能赋值给
never(除了never自身); -
never可以赋值给任意类型(因为它是所有类型的子类型); - 不会有任何实际值属于
never类型。
let n: never;
let num: number = 123;
let u: unknown = "hello";
let v: void = undefined;
// 1. 任何类型都不能赋值给 never(除了自身)
n = num; // ❌ 报错:number 不能赋值给 never
n = u; // ❌ 报错:unknown 不能赋值给 never
n = v; // ❌ 报错:void 不能赋值给 never
n = undefined; // ❌ 报错:undefined 也不行
n = n; // ✅ 仅自身可赋值
// 2. never 可以赋值给任意类型
num = n; // ✅ 正常
u = n; // ✅ 正常
v = n; // ✅ 正常
- 泛型的边界约束: 通过泛型约束让不满足条件的泛型类型变为
never,从而达到限制类型范围的目的。
// 定义泛型:仅允许 T 为 string 类型,否则 T 为 never
type OnlyString<T> = T extends string ? T : never;
// 满足条件:T 为 string,结果正常
type Str1 = OnlyString<"hello">; // Str1 = "hello"
type Str2 = OnlyString<string>; // Str2 = string
// 不满足条件:T 为非 string,结果为 never
type Num = OnlyString<number>; // Num = never
type Bool = OnlyString<boolean>; // Bool = never
type Unk = OnlyString<unknown>; // Unk = never
// 实际使用:强制函数参数只能是 string 类型
function printStr<T>(val: OnlyString<T>) {
console.log(val);
}
printStr("hello"); // ✅ 正常
printStr(123); // ❌ 报错:number 不能赋值给 never
测试用例
/* _____________ 测试用例 _____________ */
import type { Equal, Expect } from '@type-challenges/utils'
type cases = [
Expect<Equal<First<[3, 2, 1]>, 3>>,
Expect<Equal<First<[() => 123, { a: string }]>, () => 123>>,
Expect<Equal<First<[]>, never>>,
Expect<Equal<First<[undefined]>, undefined>>,
]
type errors = [
// @ts-expect-error
First<'notArray'>,
// @ts-expect-error
First<{ 0: 'arrayLike' }>,
]
相关链接
分享你的解答:tsch.js.org/14/answer/z… 查看解答:tsch.js.org/14/solution… 更多题目:tsch.js.org/zh-CN
下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。
实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。
去年我国查办网络不正当竞争案件1932件,典型案例公布
Vue-组件通信全攻略
前言
在 Vue 开发中,组件通信是构建复杂应用的基础。随着 Vue 3 的普及,通信方式发生了不少变化(如 defineProps 的引入、EventBus 的退场)。本文将对比 Vue 2 与 Vue 3,带你梳理最常用的 5 种通信方案。
一、 父子组件通信:最基础的单向数据流
这是最常用的通信方式,遵循“Props 向下传递,Emit 向上通知”的原则。
1. Vue 2 经典写法
-
接收:使用
props选项。 -
发送:使用
this.$emit。
2. Vue 3 + TS 标准写法
在 Vue 3 <script setup> 中,我们使用 defineProps 和 defineEmits。
父组件:Parent.vue
<template>
<ChildComponent :id="currentId" @childEvent="handleChild" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './Child.vue';
const currentId = ref<string>('001');
const handleChild = (msg: string) => {
console.log('接收到子组件消息:', msg);
};
</script>
子组件:Child.vue
<script setup lang="ts">
// 使用 TS 类型定义 Props
const props = defineProps<{
id: string
}>();
// 使用 TS 定义 Emits,具备更好的类型检查
const emit = defineEmits<{
(e: 'childEvent', args: string): void;
}>();
const sendMessage = () => {
emit('childEvent', '这是来自子组件的参数');
};
</script>
二、 跨级调用:通过 Ref 访问实例
有时父组件需要直接调用子组件的内部方法。
1. Vue 2 模式
直接通过 this.$refs.childRef.someMethod() 调用。
2. Vue 3 模式(显式暴露)
Vue 3 的组件默认是关闭的。如果父组件想访问子组件的方法,子组件必须使用 defineExpose。
子组件:Child.vue
<script setup lang="ts">
const childFunc = () => {
console.log('子组件方法被调用');
};
// 必须手动暴露,父组件才能访问
defineExpose({
childFunc
});
</script>
父组件:Parent.vue
<template>
<Child ref="childRef" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import Child from './Child.vue';
// 这里的类型定义有助于获得代码提示
const childRef = ref<InstanceType<typeof Child> | null>(null);
onMounted(() => {
childRef.value?.childFunc();
});
</script>
三、 非父子组件通信:事件总线 (EventBus)
1. Vue 2 做法
利用一个新的 Vue 实例作为中央调度器。
import Vue from 'vue';
export const EventBus = new Vue();
// 组件 A 发送
EventBus.$emit('event', data);
// 组件 B 接收
EventBus.$on('event', (data) => { ... });
2. Vue 3 重要变更
Vue 3 官方已移除了 $on、$off 和 $once 方法,因此不再支持直接通过 Vue 实例创建 EventBus。
-
官方推荐方案:使用第三方库
mitt或tiny-emitter。 -
补充:如果逻辑简单,可以使用 Vue 3 的
provide/inject实现跨级通信。
provide / inject 示例:
- 祖先组件:提供数据 (
App.vue)
<template>
<div class="ancestor">
<h1>祖先组件</h1>
<p>当前主题:{{ theme }}</p>
<Middle />
</div>
</template>
<script setup lang="ts">
import { ref, provide } from 'vue';
import Middle from './Middle.vue';
// 1. 定义响应式数据
const theme = ref<'light' | 'dark'>('light');
// 2. 定义修改数据的方法(推荐在提供者内部定义,保证数据流向清晰)
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
// 3. 注入 key 和对应的值/方法
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
-
中间组件:无需操作 (
Middle.vue)中间组件不需要显式接收
theme,直接透传即可 -
后代组件:注入并使用 (
DeepChild.vue)
<template>
<div class="descendant">
<h3>深层子组件</h3>
<p>接收到的主题:{{ theme }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>
<script setup lang="ts">
import { inject } from 'vue';
// 使用 inject 获取,第二个参数为默认值(可选)
const theme = inject('theme');
const toggleTheme = inject<() => void>('toggleTheme');
</script>
四、 集中式状态管理:Vuex 与 Pinia
当应用变得庞大,组件间的关系交织成网时,我们需要一个“单一事实来源”。
-
Vuex:Vue 2 时代的标准。基于 Mutation(同步)和 Action(异步)。
-
Pinia:Vue 3 的官方推荐。
- 优势:更完美的 TS 支持、没有 Mutation 的繁琐逻辑、极其轻量。
-
核心:
state、getters、actions。
Pinia 示例:
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
name: '张三',
age: 18
}),
actions: {
updateName(newName: string) {
this.name = newName;
}
}
});
五、 总结与纠错
-
安全性建议:在使用
defineExpose时,尽量只暴露必要的接口,遵循最小暴露原则。 -
EventBus 警示:Vue 3 开发者请注意,不要再尝试使用
new Vue()来做事件总线,应当转向 Pinia 或全局状态。
type-challenges(ts类型体操): 12 - 可串联构造器
12 - 可串联构造器
by Anthony Fu (@antfu) #中等 #application
题目
在 JavaScript 中我们经常会使用可串联(Chainable/Pipeline)的函数构造一个对象,但在 TypeScript 中,你能合理的给它赋上类型吗?
在这个挑战中,你可以使用任意你喜欢的方式实现这个类型 - Interface, Type 或 Class 都行。你需要提供两个函数 option(key, value) 和 get()。在 option 中你需要使用提供的 key 和 value 扩展当前的对象类型,通过 get 获取最终结果。
例如
declare const config: Chainable
const result = config
.option('foo', 123)
.option('name', 'type-challenges')
.option('bar', { value: 'Hello World' })
.get()
// 期望 result 的类型是:
interface Result {
foo: number
name: string
bar: {
value: string
}
}
你只需要在类型层面实现这个功能 - 不需要实现任何 TS/JS 的实际逻辑。
你可以假设 key 只接受字符串而 value 接受任何类型,你只需要暴露它传递的类型而不需要进行任何处理。同样的 key 只会被使用一次。
在 Github 上查看:tsch.js.org/12/zh-CN
代码
/* _____________ 你的代码 _____________ */
/**
* 定义可串联构造器的类型
* @template T 当前构造器对象的状态,默认为空对象
*/
type Chainable<T = {}> = {
/**
* 用于扩展或修改当前对象的方法
* @template K 要添加或修改的键,必须是字符串类型
* @template V 要添加或修改的值的类型
* @param key 要添加或修改的键,根据情况可能为 never 或 K
* @param value 要添加或修改的值
* @returns 一个新的 Chainable 实例,包含更新后的对象状态
*/
option: <K extends string, V>(key: K extends keyof T ? V extends T[K] ? never : K : K, value: V) => Chainable<Omit<T, K> & Record<K, V>>
/**
* 获取当前构造器对象的最终状态
* @returns 当前对象的状态
*/
get(): T
}
关键解释:
-
Chainable<T>:泛型参数,代表当前构造器对象的状态,默认为空对象; -
option(key, value):方法,用于扩展或修改当前对象的状态;-
K extends string:约束K必须是字符串类型; -
V:要添加或修改的值的类型; -
key: K extends keyof T ? V extends T[K] ? never : K : K:约束key必须是T中不存在的属性名,或者value类型与T[K]不同的属性名; -
value: V:要添加或修改的值; -
Chainable<Omit<T, K> & Record<K, V>>:返回一个新的Chainable实例,包含更新后的对象状态;
-
-
get():方法,用于获取当前构造器对象的最终状态;-
T:当前构造器对象的状态。
-
相关知识点
extends
| 使用维度 | 核心作用 | 示例场景 |
|---|---|---|
| 类型维度 | 做类型约束或条件判断(类型编程核心) | 限定泛型范围、判断类型是否兼容、提取类型片段 |
| 语法维度 | 做继承(复用已有结构) | 接口继承、类继承 |
extends 做类型约束或条件判断
- 泛型约束:限定泛型的取值范围
// 约束 T 必须是「拥有 length 属性」的类型(比如 string/数组)
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// 合法调用(符合约束)
getLength("hello"); // ✅ string 有 length,返回 5
getLength([1, 2, 3]); // ✅ 数组有 length,返回 3
// 非法调用(超出约束)
getLength(123); // ❌ 报错:number 没有 length 属性
- 条件类型:类型版 三元运算符
// 基础示例:判断类型是否为字符串
type IsString<T> = T extends string ? true : false;
type A = IsString<"test">; // true(符合)
type B = IsString<123>; // false(不符合)
分布式条件类型(联合类型专用): 当 T 是联合类型时,extends 会自动拆分联合类型的每个成员,逐个判断后再合并结果。
type Union = string | number | boolean;
// 拆分逻辑:string→string,number→never,boolean→never → 合并为 string
type OnlyString<T> = T extends string ? T : never;
type Result = OnlyString<Union>; // Result = string
注意:只有泛型参数是 裸类型(没有被 []/{} 包裹)时,才会触发分布式判断:
// 包裹后不触发分布式,整体判断 [string|number] 是否兼容 [string]
type NoDist<T> = [T] extends [string] ? T : never;
type Result2 = NoDist<Union>; // never(整体不兼容)
- 配合
infer:提取类型片段(黄金组合)
// 提取 Promise 的返回值类型
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type C = UnwrapPromise<Promise<string>>; // string(提取成功)
type D = UnwrapPromise<number>; // number(不满足条件,返回原类型)
extends 做继承(复用已有结构)
- 接口继承:复用 + 扩展属性
// 基础接口
interface User {
id: number;
name: string;
}
// 继承 User,并扩展新属性
interface Admin extends User {
role: "admin" | "super_admin"; // 新增权限属性
}
// 必须包含继承的 + 扩展的所有属性
const admin: Admin = {
id: 1,
name: "张三",
role: "admin"
};
// 多接口继承
interface HasAge { age: number; }
interface Student extends User, HasAge {
className: string; // 同时继承 User + HasAge
}
- 类继承:复用父类的属性 / 方法
class Parent {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi() {
console.log(`Hi, ${this.name}`);
}
}
// 继承 Parent 类
class Child extends Parent {
age: number;
constructor(name: string, age: number) {
super(name); // 必须调用父类构造函数(初始化父类属性)
this.age = age;
}
// 重写父类方法
sayHi() {
super.sayHi(); // 调用父类原方法
console.log(`I'm ${this.age} years old`);
}
}
const child = new Child("李四", 10);
child.sayHi(); // 输出:Hi, 李四 → I'm 10 years old
补充:类实现接口用 implements(不是 extends)
// 定义接口(契约:规定必须有 id、name 属性,以及 greet 方法)
interface Person {
id: number;
name: string;
greet(): void; // 仅定义方法签名,无实现
}
// 类实现接口(必须严格遵守契约)
class Employee implements Person {
// 必须实现接口的所有属性
id: number;
name: string;
// 构造函数初始化属性
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
// 必须实现接口的 greet 方法(具体实现由类自己定义)
greet() {
console.log(`Hi, I'm ${this.name}, ID: ${this.id}`);
}
}
// 实例化使用
const emp = new Employee(1, "张三");
emp.greet(); // 输出:Hi, I'm 张三, ID: 1
// 接口1:基础信息
interface Identifiable {
id: number;
getId(): number;
}
// 接口2:可打印
interface Printable {
printInfo(): void;
}
// 类同时实现两个接口(必须实现所有接口的成员)
class Product implements Identifiable, Printable {
id: number;
name: string; // 类可扩展接口外的属性
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
// 实现 Identifiable 的方法
getId(): number {
return this.id;
}
// 实现 Printable 的方法
printInfo() {
console.log(`Product: ${this.name}, ID: ${this.getId()}`);
}
}
const product = new Product(100, "手机");
console.log(product.getId()); // 100
product.printInfo(); // Product: 手机, ID: 100
keyof
keyof 运算符用于获取一个类型(接口、类型别名、对象类型等)的所有公共属性名,并返回这些属性名组成的联合类型。
例如:
interface Todo {
title: string
description: string
completed: boolean
}
type TodoKeys = keyof Todo // "title" | "description" | "completed"
Omit
Omit<T, K> 用于从类型 T 中排除 K 中的属性,返回一个新类型。
例如:
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = Omit<Todo, 'description'>
// TodoPreview 类型为:
// {
// title: string
// completed: boolean
// }
&
& 交叉类型运算符用于将多个类型合并为一个新类型,它会将所有属性合并到新类型中。
例如:
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = Omit<Todo, 'description'> & {
time: Date
}
// TodoPreview 类型为:
// {
// title: string
// completed: boolean
// time: Date
// }
基础类型的交叉,只有类型完全一致时才会保留原类型,类型不一致时会得到 never 。
type A = number & string // never
type B = number & boolean // never
type C = number & symbol // never
type D = string & boolean // never
type E = string & symbol // never
type F = boolean & symbol // never
同名属性的类型冲突时,会得到 never 。
interface A {
x: string; // 同名属性,类型 string
}
interface B {
x: number; // 同名属性,类型 number
}
type C = A & B;
// C 的 x 类型为 string & number → never
const c: C = {
x: 123, // 报错:类型 number 不能赋值给 never
x: "abc" // 同样报错
};
Record
Record<K, T> 是用于定义键值对结构对象类型,能快速指定对象的键类型和值的统一类型。
第一个参数 K(键类型):必须是 string | number | symbol 及其子类型(比如字符串字面量、数字字面量、联合类型),否则会报错。
第二个参数 T(值类型):可以是任意类型(基础类型、对象类型、函数类型等)。
- 字符串键 + 基础类型值
// 用 Record 定义:键是 string,值是 number
type ScoreMap = Record<string, number>;
// 等价于手动写索引签名:{ [key: string]: number }
type ScoreMap2 = { [key: string]: number };
// 正确使用:所有键的值必须是数字
const studentScores: ScoreMap = {
2: 90, // 数字字面量键会自动转为字符串,合法
"李四": 85,
wangwu: 95
};
- 字面量联合键 + 基础类型值:用字符串 / 数字字面量联合类型作为键,定义固定键、统一值类型的映射表(如状态码、枚举映射、地区编码),TS 会严格校验键的合法性(只能是联合类型中的值)
// 固定键:联合类型(字符串字面量)
type UserRole = "admin" | "editor" | "visitor";
// Record 定义:键只能是 UserRole 中的值,值是 string(角色描述)
type RoleDesc = Record<UserRole, string>;
// 正确使用:必须包含所有固定键,值为字符串
const roleDescription: RoleDesc = {
admin: "超级管理员,拥有所有权限",
editor: "内容编辑,可修改文章",
visitor: "游客,仅可查看内容"
};
// 错误示例1:缺少键(editor)→ TS 报错
const err1: RoleDesc = { admin: "xxx", visitor: "xxx" };
// 错误示例2:多余键(test)→ TS 报错
const err2: RoleDesc = { admin: "xxx", editor: "xxx", visitor: "xxx", test: "xxx" };
// 错误示例3:值类型错误(数字)→ TS 报错
const err3: RoleDesc = { admin: 123, editor: "xxx", visitor: "xxx" };
Record + Partial → 固定键,值类型可选(部分赋值)
type UserRole = "admin" | "editor" | "visitor";
// 需求:固定角色键,允许部分赋值(不是所有角色都需要写描述)
type PartialRoleDesc = Partial<Record<UserRole, string>>;
// 正确使用:可包含任意数量的键(0个、1个、多个、全部)
const emptyDesc: PartialRoleDesc = {}; // 正常
const partialDesc: PartialRoleDesc = { admin: "超级管理员" }; // 正常
const fullDesc: PartialRoleDesc = { admin: "xxx", editor: "xxx", visitor: "xxx" }; // 正常
测试用例
/* _____________ 测试用例 _____________ */
import type { Alike, Expect } from '@type-challenges/utils'
declare const a: Chainable
const result1 = a
.option('foo', 123)
.option('bar', { value: 'Hello World' })
.option('name', 'type-challenges')
.get()
const result2 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 'last name')
.get()
const result3 = a
.option('name', 'another name')
.option('name', 123)
.get()
type cases = [
Expect<Alike<typeof result1, Expected1>>,
Expect<Alike<typeof result2, Expected2>>,
Expect<Alike<typeof result3, Expected3>>,
]
type Expected1 = {
foo: number
bar: {
value: string
}
name: string
}
type Expected2 = {
name: string
}
type Expected3 = {
name: number
}
相关链接
分享你的解答:tsch.js.org/12/answer/z… 查看解答:tsch.js.org/12/solution… 更多题目:tsch.js.org/zh-CN
下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。
实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。
《实时渲染》第2章-图形渲染管线-2.5像素处理
实时渲染
2. 图形渲染管线
2.5 像素处理
这个阶段是所有先前阶段组合的结果,并且已经找到了在三角形或其他图元内被考虑的所有像素。像素处理阶段分为像素着色和合并,如图2.8右侧所示。像素处理是对图元内部的像素或样本执行逐像素或逐样本计算和操作的阶段。
2.5.1 像素着色
此处执行的任何逐像素着色计算,是使用内插着色数据作为输入的。最终结果是将一种或多种颜色传递到下一阶段。与通常由专用的,硬连线的芯片执行的三角形设置和遍历阶段不同,像素着色阶段由可编程GPU内核执行。为此,程序员为像素着色器(或在OpenGL中称为片元着色器)提供了一个程序,该程序可以包含任何所需的计算。这里可以使用多种技术,其中最重要的一种是纹理贴图。纹理在第6章中进行了更详细的论述。简单地说,纹理对象意味着将一个或多个图像“粘合”到该对象上,用于各种目的。图2.9描述了此过程的一个简单示例。图像可以是一维、二维或三维,其中二维图像最为常见。最简单的情况是,最终产品是每个片元的颜色值,这些值被传递到下一个子阶段。
图2.9. 没有纹理的龙模型显示在左上角。图像纹理中的片段“粘”在龙上,结果显示在左下角。
2.5.2 合并
每个像素的信息存储在颜色缓冲区中,它是一个矩形的颜色数组(每种颜色具有红色、绿色和蓝色分量)。合并阶段的职责是将像素着色阶段产生的片元颜色与当前存储在缓冲区中的颜色相结合。此阶段也称为ROP,表示“(管线)光栅操作”或“渲染输出单元”,具体取决于你访问的对象。与着色阶段不同,执行此阶段的GPU子单元通常不是完全可编程的。但是,它是高度可配置的,可以实现各种效果。
此阶段还负责解决可见性问题。这意味着当整个场景被渲染后,颜色缓冲区应该包含场景中从相机的角度可见的图元的颜色。对于大多数甚至所有图形硬件,这是通过z缓冲区(也称为深度缓冲区)算法完成的[238]。z缓冲区的大小和形状与颜色缓冲区相同,并且对于每个像素,它将z值存储到当前最接近的图元。这意味着当一个图元被渲染到某个像素时,该图元在该像素上的z值被计算并与同一像素的z缓冲区的内容进行比较。如果新的z值小于z缓冲区中的 z 值,则正在渲染的图元比之前在该像素处最靠近相机的图元更靠近相机。因此,该像素的z值和颜色将使用正在绘制的图元的z值和颜色进行更新。如果计算出的z值大于z缓冲区中的z值,则颜色缓冲区和z缓冲区保持不变。z缓冲区算法很简单,具有O(n)收敛性(其中n是正在渲染的图元数量),并且可以适用于为每个(相关)像素计算z值的任何绘图图元。另请注意,该算法允许以任何顺序呈现大多数图元,这是其流行的另一个原因。但是,z 缓冲区仅在屏幕上的每个点存储单个深度,因此它不能用于部分透明的图元。透明图元必须在所有不透明基元之后渲染,并以从后到前的顺序呈现,或使用单独的与顺序无关的算法(第5.5节)。透明度是基本z缓冲区算法的主要弱点之一。
我们已经提到颜色缓冲区用于存储颜色,而z缓冲区存储每个像素的z值。但是,还有其他通道和缓冲区可用于过滤和捕获片元信息。Alpha通道与颜色缓冲区相关联,并为每个像素存储相关的不透明度值(第5.5节)。在较旧的API中,alpha通道还用于通过alpha测试功能有选择地丢弃像素。如今,可以将丢弃操作插入到像素着色器程序中,并且可以使用任何类型的计算来触发丢弃。此类测试可用于确保完全透明的片段不会影响z缓冲区(第6.6节)。
模板缓冲区是一个离屏缓冲区,用于记录渲染图元的位置。它通常包含每像素 8 位。可以使用各种函数将图元渲染到模板缓冲区中,然后可以使用缓冲区的内容来控制渲染到颜色缓冲区和z缓冲区中。例如,假设一个实心圆已被绘制到模板缓冲区中。这可以与允许将后续图元渲染到仅存在圆圈的颜色缓冲区中的运算符结合使用。模板缓冲区可以成为生成某些特殊效果的强大工具。管线末端的所有这些功能都称为光栅操作 (ROP) 或混合操作。可以将当前在颜色缓冲区中的颜色与三角形内正在处理的像素的颜色混合。这可以启用诸如透明度或颜色样本累积等效果。如前所述,混合操作通常可以使用API进行配置,而不是完全可编程的。但是,某些API支持光栅顺序视图,也称为像素着色器排序,可实现可编程混合功能。
帧缓冲区通常由系统上的所有缓冲区组成。
当图元到达并通过光栅化阶段时,从相机的角度上看,这些可见的图元将会显示在屏幕上。屏幕显示颜色缓冲区的内容。为了避免让人类观察者在被光栅化并发送到屏幕时看到图元,使用了双缓冲。这意味着场景的渲染发生在屏幕外的后台缓冲区中。在后台缓冲区中渲染场景后,后台缓冲区的内容将与之前显示在屏幕上的前台缓冲区的内容交换。交换通常发生在垂直重描期间,这是安全的时候。