星火

24 阅读42分钟

vue3的setup作用,为啥可以直接使用defineprops等,如何实现的?

Vue3 中setup的作用

setup是 Vue3 组合式 API 的核心入口,它是组件创建前(在beforeCreate钩子前)执行的函数,主要作用包括:

  1. 变量 / 方法暴露:返回的对象中的属性和方法可直接在模板中使用;
  2. 组合逻辑复用:将组件逻辑拆分为独立的组合函数,在setup中调用实现复用;
  3. 生命周期钩子注册:通过onMountedonUpdated等函数注册生命周期逻辑;
  4. 访问组件上下文:可通过参数(propscontext)获取组件的属性、插槽、emit 等;
  5. 响应式数据创建:结合refreactive等 API 创建响应式数据。

setup执行时,组件实例尚未创建,因此无法访问thisthisundefined)。

为何能直接使用defineProps/defineEmits

definePropsdefineEmitsdefineExposewithDefaults等并非全局函数,而是 Vue3 为单文件组件(SFC)  提供的编译器宏(Compiler Macros) ,特性如下:

  1. 无需导入:Vue 的 SFC 编译器会在编译阶段自动识别这些宏,无需手动import
  2. 编译时处理:宏的调用会被编译器转换为底层的props/emits定义逻辑,运行时不存在实际的函数调用;
  3. 类型支持:在<script setup>中使用时,可结合 TypeScript 提供类型推导,增强类型安全性。

例如,<script setup>中写:

vue

<script setup>
const props = defineProps({ msg: String })
const emit = defineEmits(['change'])
</script>

编译器会将其转换为:

javascript

运行

export default {
  props: { msg: String },
  emits: ['change'],
  setup(props, { emit }) {
    // ... 原setup逻辑
    return { props, emit }
  }
}

实现原理

1. <script setup>的编译处理

Vue 的 SFC 编译器(@vue/compiler-sfc)会对<script setup>进行特殊处理:

  • 解析脚本内容,识别defineProps/defineEmits等宏的调用;
  • 将宏的参数(如 props 的类型定义)提取为组件选项(props/emits);
  • setup函数的内容包裹,注入propscontext参数,并将宏返回的变量(如propsemit)暴露给模板。
2. 宏的底层映射
  • defineProps:编译后映射到组件的props选项,运行时通过setup的第一个参数props访问;
  • defineEmits:编译后映射到组件的emits选项,运行时通过setup的第二个参数context.emit访问;
  • defineExpose:编译后将指定属性添加到组件实例,允许父组件通过模板引用访问;
  • withDefaults:为defineProps的类型定义添加默认值,编译后生成propsdefault选项。
3. 作用域隔离

<script setup>中的代码会被编译为组件setup函数的内部逻辑,宏的调用被限制在当前组件作用域内,避免全局污染。编译器会确保宏的返回值(如props对象)能被模板和组件内部访问。

总结

  • setup是组合式 API 的入口,负责组件逻辑的组织和暴露;
  • defineProps等是 SFC 编译器提供的宏,无需导入是因为编译阶段被处理;
  • 实现核心是 Vue 编译器对<script setup>的特殊解析,将宏转换为组件选项和setup参数,从而简化语法并保持类型支持。

ts的type的安全保护?

TypeScript 的类型安全保护(Type Guards)是一种用于在运行时检查类型,从而让 TypeScript 编译器在特定代码块中推断出更具体类型的机制。它能帮助开发者避免类型错误,提升代码的类型安全性。以下是 TypeScript 中常见的类型安全保护方式及实现原理:

一、typeof 类型保护

作用:用于检查基本类型(string/number/boolean/symbol)。原理:利用 JavaScript 的 typeof 操作符,TypeScript 会识别 typeof 判断语句,并在后续代码块中推断出具体类型。

typescript

运行

function printValue(value: string | number) {
  if (typeof value === 'string') {
    // 此处 TypeScript 推断 value 为 string 类型
    console.log(value.toUpperCase()); 
  } else {
    // 此处推断为 number 类型
    console.log(value.toFixed(2)); 
  }
}

二、instanceof 类型保护

作用:用于检查对象是否是某个类的实例(适用于类 / 构造函数创建的对象)。原理:通过 instanceof 操作符判断对象的原型链,TypeScript 会根据结果推断对象类型。

typescript

运行

class Animal { name: string; }
class Dog extends Animal { bark() {} }

function handleAnimal(animal: Animal | Dog) {
  if (animal instanceof Dog) {
    // 推断 animal 为 Dog 类型
    animal.bark(); 
  } else {
    // 推断为 Animal 类型
    console.log(animal.name);
  }
}

三、自定义类型保护函数

作用:通过返回特定格式的布尔值(arg is Type),手动告诉 TypeScript 类型推断规则,适用于复杂类型或自定义类型检查。原理:函数返回类型为 parameter is Type,TypeScript 会识别这种返回类型,并在函数返回 true 时,将参数推断为指定类型。

typescript

运行

interface Cat { meow(): void; }
interface Dog { bark(): void; }

// 自定义类型保护函数
function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}

function handlePet(pet: Cat | Dog) {
  if (isCat(pet)) {
    // 推断 pet 为 Cat 类型
    pet.meow(); 
  } else {
    pet.bark(); 
  }
}

四、in 操作符类型保护

作用:用于检查对象是否包含某个属性,适用于区分不同接口 / 类型的对象。原理:通过 in 操作符判断属性存在性,TypeScript 会据此推断对象类型。

typescript

运行

interface Fish { swim(): void; }
interface Bird { fly(): void; }

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    // 推断 animal 为 Fish 类型
    animal.swim(); 
  } else {
    animal.fly(); 
  }
}

五、字面量类型保护

作用:用于区分字面量类型(如字符串字面量、数字字面量)。原理:通过检查字面量的值,TypeScript 推断具体的字面量类型。

typescript

运行

type Status = 'success' | 'error' | 'loading';

function handleStatus(status: Status) {
  if (status === 'success') {
    console.log('成功'); 
  } else if (status === 'error') {
    console.log('失败'); 
  }
}

六、可辨识联合类型(Discriminated Unions)

作用:针对包含共同字面量属性的联合类型,通过该属性快速区分类型(常用于状态管理、事件处理)。原理:联合类型中的每个类型都有一个唯一的 “辨识属性”(如 type),通过检查该属性值,TypeScript 推断具体类型。

typescript

运行

interface SuccessResponse {
  type: 'success';
  data: string;
}
interface ErrorResponse {
  type: 'error';
  message: string;
}
type Response = SuccessResponse | ErrorResponse;

function handleResponse(res: Response) {
  if (res.type === 'success') {
    // 推断为 SuccessResponse
    console.log(res.data); 
  } else {
    // 推断为 ErrorResponse
    console.log(res.message); 
  }
}

七、非空类型保护(! 和 ??

作用:排除 null/undefined 类型,确保变量非空。

  • value!:断言变量非空(慎用,需确保运行时确实非空);
  • value ?? defaultValue:空值合并,避免 null/undefined

typescript

运行

function printName(name: string | null) {
  // 非空断言,告诉 TypeScript name 非 null
  console.log(name!.toUpperCase()); 
  
  // 空值合并,name 为 null 时使用默认值
  console.log((name ?? 'Unknown').toUpperCase());
}

类型保护的核心价值

  1. 运行时类型检查:弥补 TypeScript 编译时类型检查的不足,避免运行时因类型错误导致的崩溃;
  2. 精确类型推断:让 TypeScript 在代码块中推断出更具体的类型,减少类型断言(as)的使用;
  3. 代码可读性提升:明确的类型检查逻辑让代码意图更清晰。

通过合理使用类型保护,开发者可以在享受 TypeScript 类型安全的同时,编写更灵活、健壮的代码。

实际项目中使用TypeScript类型保护的经验

在实际项目中,TypeScript 类型保护的合理使用能大幅提升代码健壮性和可维护性,以下结合真实业务场景分享具体经验和实践技巧:

一、优先用 “可辨识联合类型” 处理状态 / 事件逻辑

在前端业务中,状态管理(如请求状态、组件状态)  和 事件分发 是高频场景,用 “可辨识联合类型” 替代零散的 if-else,既能保证类型安全,又能让逻辑更清晰。

场景:接口请求状态处理

typescript

运行

// 定义请求状态的可辨识联合类型
type RequestState<T = any> = 
  | { type: 'idle' }
  | { type: 'loading' }
  | { type: 'success'; data: T }
  | { type: 'error'; message: string };

// 业务组件中使用
function renderRequestState(state: RequestState<User[]>) {
  switch (state.type) {
    case 'idle':
      return <div>请点击发起请求</div>;
    case 'loading':
      return <Spin />;
    case 'success':
      // 自动推断 state.data 为 User[] 类型
      return <UserList data={state.data} />;
    case 'error':
      return <Alert message={state.message} type="error" />;
    // 若遗漏 case,TypeScript 会提示“并非所有情况都有处理”
  }
}

经验

  • 为联合类型定义唯一的 type 字段(辨识属性),配合 switch-case 使用,TypeScript 会强制覆盖所有分支,避免遗漏异常场景;
  • 接口返回数据、异步操作状态等场景优先用这种模式,比单独判断 isLoading/hasError 更不易出错。

二、自定义类型保护函数封装 “复杂类型判断逻辑”

当需要判断的类型规则复杂(如嵌套对象、第三方库类型),重复写 typeof/in 会导致代码冗余,此时封装复用性强的自定义类型保护函数更高效。

场景:处理后端返回的异构列表数据

后端返回的列表可能混合多种类型(如商品、优惠券),需根据字段区分:

typescript

运行

// 定义业务类型
interface Product { type: 'product'; id: string; price: number; }
interface Coupon { type: 'coupon'; id: string; discount: number; }
type Item = Product | Coupon;

// 封装可复用的类型保护函数(可抽离到 utils 中)
export function isProduct(item: Item): item is Product {
  return item.type === 'product' && 'price' in item;
}

export function isCoupon(item: Item): item is Coupon {
  return item.type === 'coupon' && 'discount' in item;
}

// 业务中使用
function renderItem(item: Item) {
  if (isProduct(item)) {
    return <ProductCard price={item.price} />; // 类型安全
  } else if (isCoupon(item)) {
    return <CouponTag discount={item.discount} />;
  }
}

经验

  • 自定义类型保护函数命名统一(如 isXxx),参数和返回值明确(arg is Type),方便团队协作;
  • 涉及第三方库类型(如 axios 的 AxiosResponsevue 的 Ref)时,封装类型保护函数可隐藏底层细节,降低耦合。

三、用 in 操作符快速区分接口类型

当类型差异仅在于 “是否包含某个属性” 时,in 操作符比自定义函数更简洁,适合轻量场景。

场景:区分用户类型(普通用户 / 管理员)

typescript

运行

interface User { role: 'user'; name: string; }
interface Admin { role: 'admin'; name: string; permissions: string[]; }
type UserOrAdmin = User | Admin;

function checkPermissions(user: UserOrAdmin) {
  if ('permissions' in user) {
    // 自动推断为 Admin 类型
    return user.permissions.includes('delete');
  }
  return false;
}

经验

  • 避免用 in 判断嵌套过深的属性(如 'a' in b.c.d),可读性差;
  • 结合解构赋值使用:if ('permissions' in user && user.permissions.length),一步完成存在性和有效性检查。

四、非空类型保护:避免 “空值地狱”

实际项目中 null/undefined 是高频错误源,合理使用非空保护能减少 Cannot read property 'xxx' of undefined 错误。

场景:处理可选属性 / API 返回的空值

typescript

运行

interface Order {
  id: string;
  user?: { name?: string }; // 嵌套可选属性
}

function getUserName(order: Order): string {
  // 链式判断:避免直接访问 order.user.name
  if (order.user && order.user.name) {
    return order.user.name;
  }
  return '匿名用户';
}

// 或用空值合并(??)+ 可选链(?.)
function getUserNameShort(order: Order): string {
  return order.user?.name ?? '匿名用户';
}

经验

  • 慎用非空断言(!),仅在确定值非空时使用(如 DOM 元素已通过 ref 绑定且一定存在);
  • 结合 Optional Chaining?.)和 Nullish Coalescing??),简化嵌套可选属性的判断,代码更简洁。

五、类型保护与泛型结合:处理通用组件 / 工具函数

通用组件(如表格、弹窗)或工具函数常需兼容多种类型,类型保护 + 泛型能兼顾灵活性和类型安全。

场景:通用表格组件的列渲染

typescript

运行

// 泛型定义表格列配置
type TableColumn<T> = 
  | { type: 'text'; key: keyof T }
  | { type: 'action'; render: (row: T) => React.ReactNode };

// 表格组件中根据列类型渲染
function TableCell<T>(row: T, column: TableColumn<T>) {
  if (column.type === 'text') {
    // 自动推断 row[column.key] 为 T 的对应属性类型
    return <span>{row[column.key]}</span>;
  } else {
    return column.render(row);
  }
}

// 使用时传入具体类型
interface User { id: string; name: string; }
const columns: TableColumn<User>[] = [
  { type: 'text', key: 'name' },
  { type: 'action', render: (row) => <Button onClick={() => deleteUser(row.id)}>删除</Button> }
];

经验

  • 泛型参数结合类型保护,能让 TypeScript 推断出更具体的类型(如 row[column.key] 的类型);
  • 工具函数(如深拷贝、数据校验)中,用类型保护处理不同泛型分支,避免 any 类型。

六、避免过度类型保护:平衡类型安全与代码复杂度

类型保护虽好,但过度使用会导致代码冗余。以下场景可简化:

  1. 简单联合类型:若类型差异明显,直接用 typeof/instanceof 即可,无需封装函数;
  2. 类型断言的合理使用:当上下文已明确类型(如从 Array.find 获取的元素),可偶尔用 as Type 替代冗长的保护逻辑;
  3. 利用 TypeScript 4.9+ 的 satisfies 操作符:校验类型但保留原始类型推断,避免过度窄化。

总结:实战中的核心原则

  1. 场景化选择:简单类型用 typeof/in,复杂类型用自定义保护函数,状态逻辑用可辨识联合类型;
  2. 复用优先:将通用的类型保护逻辑抽离到工具库(如 src/utils/type-guards.ts),避免重复代码;
  3. 类型安全与可读性并重:不要为了 “纯类型安全” 写晦涩的代码,团队能理解的方案才是最优解;
  4. 结合编辑器提示:TypeScript 的报错和自动补全是 “盟友”,利用类型保护让编辑器更智能,减少手动调试。

通过这些实践,既能发挥 TypeScript 的类型优势,又能避免陷入 “类型体操” 的陷阱,让代码在安全和效率之间找到平衡。

总结

TypeScript 的类型安全保护通过以下机制实现:

  1. 编译时检查 - 在代码运行前捕获类型错误
  2. 类型推断 - 自动推导变量和函数返回值的类型
  3. 类型收窄 - 通过条件判断缩小类型范围
  4. 泛型约束 - 提供灵活的类型参数约束
  5. 严格的空值检查 - 防止空指针错误
  6. 只读类型 - 保护数据不可变性
  7. 高级类型特性 - 模板字面量、条件类型等提供更精确的类型控制

这些机制共同作用,大大提高了代码的可靠性、可维护性和开发体验。

ts的类型收紧?

TypeScript 的类型收紧(Type Narrowing)  是指编译器根据代码中的条件判断、类型检查等逻辑,将一个宽泛的类型(如联合类型、父类型)推断为更具体、更精确的子类型的过程。它是实现类型安全的核心机制之一,与类型保护(Type Guards)紧密关联 —— 类型保护是触发类型收紧的手段,类型收紧是类型保护的结果。

一、类型收紧的常见触发场景

1. 通过 typeof 触发基本类型收紧

当使用 typeof 检查基本类型时,TypeScript 会在条件分支中将变量类型收紧为对应的具体类型:

typescript

运行

let value: string | number;

if (typeof value === "string") {
  // value 被收紧为 string 类型
  value.toUpperCase(); // ✅ 合法
} else {
  // value 被收紧为 number 类型
  value.toFixed(2); // ✅ 合法
}
2. 通过 instanceof 触发类 / 对象类型收紧

针对类实例或构造函数创建的对象,instanceof 会将类型收紧为具体的类类型:

typescript

运行

class Dog { bark() {} }
class Cat { meow() {} }

let pet: Dog | Cat = new Dog();

if (pet instanceof Dog) {
  // pet 被收紧为 Dog 类型
  pet.bark(); // ✅ 合法
}
3. 通过 in 操作符触发接口 / 对象类型收紧

检查对象是否包含某个属性时,TypeScript 会收紧为包含该属性的具体类型:

typescript

运行

interface A { x: number }
interface B { y: string }

let obj: A | B = { x: 10 };

if ("x" in obj) {
  // obj 被收紧为 A 类型
  console.log(obj.x); // ✅ 合法
}
4. 通过字面量值检查触发字面量类型收紧

对于字面量联合类型,通过值比较可将类型收紧为具体的字面量类型:

typescript

运行

type Status = "success" | "error" | "loading";
let status: Status = "success";

if (status === "success") {
  // status 被收紧为 "success" 字面量类型
  console.log("操作成功");
}
5. 通过自定义类型保护函数触发收紧

返回 arg is Type 的自定义函数会明确告诉编译器:当函数返回 true 时,参数类型可收紧为 Type

typescript

运行

interface Fish { swim: () => void }
interface Bird { fly: () => void }

function isFish(animal: Fish | Bird): animal is Fish {
  return "swim" in animal;
}

let animal: Fish | Bird = { swim: () => {} };

if (isFish(animal)) {
  // animal 被收紧为 Fish 类型
  animal.swim(); // ✅ 合法
}
6. 通过可辨识联合类型触发收紧

包含唯一 “辨识属性”(如 type 字段)的联合类型,通过检查辨识属性可快速收紧为具体类型:

typescript

运行

type Circle = { type: "circle"; radius: number };
type Square = { type: "square"; side: number };
type Shape = Circle | Square;

let shape: Shape = { type: "circle", radius: 5 };

switch (shape.type) {
  case "circle":
    // shape 被收紧为 Circle 类型
    console.log(shape.radius); // ✅ 合法
    break;
  case "square":
    // shape 被收紧为 Square 类型
    console.log(shape.side); // ✅ 合法
}
7. 通过非空检查触发收紧

检查变量是否不为 null/undefined 时,TypeScript 会收紧类型以排除空值:

typescript

运行

let name: string | null = "Alice";

if (name) {
  // name 被收紧为 string 类型(排除 null)
  name.toUpperCase(); // ✅ 合法
}

二、类型收紧的边界与注意事项

1. 类型收紧的作用域限制

类型收紧仅在当前代码块(if/else/switch 分支、函数体等)内有效,外部不会受影响:

typescript

运行

let value: string | number = "hello";

if (typeof value === "string") {
  value.toUpperCase(); // ✅ 内部收紧为 string
}

value.toUpperCase(); // ❌ 外部仍为 string | number,报错
2. 复杂类型的 “部分收紧”

对于嵌套对象或交叉类型,TypeScript 可能仅收紧部分属性类型,而非整体类型:

typescript

运行

type Obj = { a: string | number } | { b: boolean };
let obj: Obj = { a: 123 };

if ("a" in obj) {
  // obj 被收紧为 { a: string | number }
  obj.a.toFixed(2); // ✅ 但 a 仍为 string | number,需进一步检查
}
3. 类型守卫的 “可靠性” 影响收紧

TypeScript 仅信任编译器认可的类型保护方式(如 typeof/instanceof/arg is Type),自定义逻辑若未遵循类型保护规则,可能无法触发收紧:

typescript

运行

let value: string | number = 10;

// 普通布尔判断无法触发收紧
if (value > 0) {
  value.toFixed(2); // ❌ 仍为 string | number,报错
}

三、实战中的类型收紧技巧

1. 利用 never 类型确保穷尽检查

在可辨识联合类型的 switch 中,通过 default 分支返回 never,可强制覆盖所有类型分支,避免遗漏:

typescript

运行

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.type) {
    case "circle":
      return Math.PI * shape.radius **2;
    case "square":
      return shape.side** 2;
    default:
      // 若遗漏分支,shape 类型为 never,触发报错
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}
2. 用类型断言辅助复杂场景的收紧

若编译器无法自动推断,但开发者明确类型时,可通过 as Type 手动收紧(需谨慎使用):

typescript

运行

let value: unknown = "hello";

if (typeof value === "string") {
  const trimmed = (value as string).trim(); // ✅ 手动辅助收紧
}
3. 结合泛型与类型保护实现通用收紧

针对泛型工具函数,通过类型保护可将泛型参数收紧为具体类型:

typescript

运行

function isArray<T>(value: T | T[]): value is T[] {
  return Array.isArray(value);
}

function processValue<T>(value: T | T[]) {
  if (isArray(value)) {
    // value 被收紧为 T[] 类型
    value.map(item => item); // ✅ 合法
  }
}

四、类型收紧的核心价值

  1. 减少类型断言:通过自动收紧避免大量 as Type 操作,代码更简洁安全;
  2. 提前暴露错误:在编译阶段识别类型不匹配的问题,而非运行时崩溃;
  3. 增强代码提示:编辑器可根据收紧后的类型提供精准的自动补全,提升开发效率。

理解类型收紧的触发逻辑和边界,能帮助你更精准地设计类型结构,写出既灵活又安全的 TypeScript 代码。

ts的return type工具?如何实现的?有什么作用?

TypeScript 中的 ReturnType<T>  是一个内置的工具类型(Utility Type),用于提取函数类型 T 的返回值类型。它是 TypeScript 类型系统中处理函数返回值的核心工具之一,广泛应用于类型推导、类型复用等场景。

一、ReturnType<T> 的基本用法

ReturnType<T> 接收一个函数类型 T 作为泛型参数,返回该函数的返回值类型:

typescript

运行

// 定义一个函数类型
type Fn = (a: number, b: string) => { name: string; age: number };

// 提取返回值类型
type FnReturn = ReturnType<Fn>; 
// 结果:{ name: string; age: number }

// 直接对函数使用
function getUser() {
  return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof getUser>; 
// 结果:{ id: number; name: string }

注意:ReturnType<T> 仅接受函数类型作为参数,若传入非函数类型(如 stringobject),TypeScript 会报错。

二、ReturnType<T> 的实现原理

ReturnType<T> 基于 TypeScript 的条件类型(Conditional Types)  和函数类型推断实现,其底层定义如下(来自 TypeScript 源码):

typescript

运行

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
拆解实现逻辑:
  1. 泛型约束T extends (...args: any) => any 确保传入的 T 必须是函数类型(否则触发类型错误);

  2. 条件类型 + 类型推断

    • T extends (...args: any) => infer R:检查 T 是否匹配 “任意参数、返回值为 R” 的函数类型;
    • infer R:使用 infer 关键字推断函数的返回值类型,并将其绑定到变量 R
    • 若匹配成功,返回 R(即函数的返回值类型);否则返回 any(兜底逻辑)。

三、ReturnType<T> 的扩展场景与作用

1. 提取动态函数的返回值类型

在处理第三方库函数、异步函数或动态生成的函数时,ReturnType<T> 可自动推导返回值类型,避免手动重复定义:

typescript

运行

// 异步函数场景
const fetchData = async () => {
  return { code: 200, data: [{ id: 1, name: "test" }] };
};
type FetchResult = ReturnType<typeof fetchData>; 
// 结果:Promise<{ code: number; data: { id: number; name: string }[] }>

// 第三方库函数
import { useState } from "react";
type StateReturn = ReturnType<typeof useState<number>>; 
// 结果:[number, React.Dispatch<React.SetStateAction<number>>]
2. 类型复用与避免硬编码

当函数返回值类型复杂时,ReturnType<T> 可复用函数的返回值类型,减少类型定义的冗余:

typescript

运行

// 原始函数
function buildUser(id: number) {
  return {
    id,
    name: `User${id}`,
    createdAt: new Date(),
    permissions: ["read", "write"] as const,
  };
}

// 复用返回值类型(无需手动定义 interface)
type User = ReturnType<typeof buildUser>;
// 结果:{ id: number; name: string; createdAt: Date; permissions: readonly ["read", "write"] }

// 基于返回值类型扩展
type AdminUser = User & { role: "admin" };
3. 结合泛型与工具类型增强灵活性

ReturnType<T> 可与其他工具类型(如 Parameters<T>Partial<T>)结合,实现更复杂的类型推导:

typescript

运行

// 获取函数参数类型 + 返回值类型
type Fn = (a: string, b: number) => boolean;
type FnParams = Parameters<Fn>; // [string, number]
type FnReturn = ReturnType<Fn>; // boolean

// 构造“参数-返回值”映射类型
type FnMap<T extends (...args: any) => any> = {
  params: Parameters<T>;
  return: ReturnType<T>;
};
type MyFnMap = FnMap<typeof buildUser>;
// 结果:{ params: [number]; return: { id: number; ... } }
4. 处理重载函数

对于重载函数,ReturnType<T> 会提取最后一个重载签名的返回值类型(TypeScript 重载的特性):

typescript

运行

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any) {
  return a + b;
}

type AddReturn = ReturnType<typeof add>; 
// 结果:string(最后一个重载的返回值类型)
5. 高级:结合条件类型过滤返回值

通过嵌套条件类型,可基于返回值类型做进一步过滤:

typescript

运行

// 提取函数返回值中不为 null/undefined 的类型
type NonNullableReturn<T extends (...args: any) => any> = NonNullable<ReturnType<T>>;

function getOptionalValue(): string | null {
  return Math.random() > 0.5 ? "hello" : null;
}
type Value = NonNullableReturn<typeof getOptionalValue>; // string

四、类似的函数相关工具类型

除了 ReturnType<T>,TypeScript 还提供了其他处理函数的工具类型,常与 ReturnType<T> 配合使用:

  • Parameters<T> :提取函数的参数类型(返回元组类型);
  • ConstructorParameters<T> :提取类构造函数的参数类型;
  • ThisParameterType<T> :提取函数的 this 参数类型;
  • OmitThisParameter<T> :移除函数的 this 参数类型。

五、总结

  • 实现核心:基于条件类型和 infer 关键字,推断函数的返回值类型;
  • 核心作用:自动提取函数返回值类型,实现类型复用、减少硬编码,提升类型推导的灵活性;
  • 适用场景:动态函数类型推导、复杂返回值复用、泛型工具类型设计等。

ReturnType<T> 是 TypeScript 类型编程中处理函数返回值的基础工具,熟练使用它能大幅提升类型代码的简洁性和可维护性。

ts的isfunction

在 TypeScript 中,isFunction 通常指用于判断值是否为函数类型的类型保护函数(或类型断言逻辑),核心作用是在运行时检查值的类型,并让 TypeScript 编译器在代码块中推断出更具体的类型(即类型收紧)。

一、为什么需要 isFunction

由于 TypeScript 的类型系统在运行时会被擦除,直接通过类型注解无法判断值的实际类型。对于可能为 functionobjectnull 等混合类型的值,需要通过运行时检查来确保类型安全,isFunction 就是为此设计的工具。

二、实现 isFunction 类型保护函数

1. 基础实现

利用 JavaScript 的 typeof 操作符判断值的类型是否为 function,并结合 TypeScript 的类型保护语法(arg is Function)让编译器识别类型:

typescript

运行

/**
 * 判断值是否为函数类型的类型保护函数
 * @param value 待检查的值
 * @returns 布尔值,同时告诉 TypeScript 编译器 value 的类型
 */
function isFunction(value: unknown): value is Function {
  return typeof value === 'function';
}
2. 更严格的函数类型判断(区分普通函数 / 箭头函数 / 类)

若需更精细的判断(如排除类构造函数),可结合 Object.prototype.toString

typescript

运行

function isStrictFunction(value: unknown): value is (...args: any[]) => any {
  const type = Object.prototype.toString.call(value);
  // 匹配普通函数、箭头函数、异步函数、生成器函数等
  return type === '[object Function]' 
    || type === '[object AsyncFunction]' 
    || type === '[object GeneratorFunction]';
}

三、isFunction 的使用场景

1. 类型收窄与安全调用

当值可能为多种类型时,用 isFunction 确保调用前是函数类型:

typescript

运行

type Callback = ((data: string) => void) | string | undefined;

function handleCallback(callback: Callback) {
  if (isFunction(callback)) {
    // TypeScript 推断 callback 为函数类型
    callback('hello'); // ✅ 安全调用
  } else {
    console.log('Callback is not a function:', callback);
  }
}
2. 处理未知类型的参数

在工具函数中接收 unknown 类型参数时,用 isFunction 做前置检查:

typescript

运行

function executeIfFunction(value: unknown, ...args: any[]) {
  if (isFunction(value)) {
    return value(...args); // ✅ 类型安全
  }
  return value;
}

executeIfFunction(() => 123); // 返回 123
executeIfFunction('test'); // 返回 'test'
3. 过滤数组中的函数

从混合类型数组中筛选出函数元素:

typescript

运行

const mixedArray = [123, 'abc', () => {}, { a: 1 }, async () => {}];
const functions = mixedArray.filter(isFunction);
// functions 类型被推断为 Function[]
functions.forEach(fn => fn()); // ✅ 安全调用

四、TypeScript 内置的函数类型工具

除了自定义 isFunction,TypeScript 还提供了内置的函数类型工具,可结合使用:

  • Function:表示任意函数类型(包含所有函数,如普通函数、类、箭头函数等);
  • (...args: any[]) => any:更精确的函数类型(排除类构造函数);
  • ReturnType<T>/Parameters<T> :提取函数的返回值 / 参数类型(需配合 isFunction 使用)。

示例:结合类型工具增强类型推导

typescript

运行

function getFunctionReturnType<T extends (...args: any[]) => any>(fn: T): ReturnType<T> {
  return fn();
}

if (isFunction(value)) {
  const result = getFunctionReturnType(value); // 自动推断返回值类型
}

五、注意事项

  1. 类构造函数的特殊性typeof class {} 会返回 function,因此 isFunction 会将类也判定为函数。若需排除类,可通过 value.prototype 或 Object.prototype.toString 进一步判断:

    typescript

    运行

    function isNotClass(value: unknown): boolean {
      return isFunction(value) && !('prototype' in value) || !value.prototype.hasOwnProperty('constructor');
    }
    
  2. 避免滥用 any:在类型保护函数中尽量使用 unknown 而非 any,保持类型安全;

  3. 结合 strictNullChecks:启用严格空检查后,isFunction 还能排除 null/undefined,进一步提升安全性。

总结

isFunction 是 TypeScript 中处理函数类型的常用工具,核心价值在于:

  • 运行时类型检查:弥补编译时类型擦除的不足;
  • 类型收窄:让编译器识别函数类型,避免调用非函数值导致的运行时错误;
  • 类型复用:结合内置工具类型实现更灵活的函数类型推导。

在实际项目中,可将 isFunction 封装到工具库(如 src/utils/type-guards.ts),作为通用类型保护函数复用。

vue的emit是异步还是同步?

Vue 中 emit 触发的事件默认是同步执行的,但在某些场景下可能表现出 “异步” 的表象,具体需结合使用场景分析:

一、默认行为:同步执行

$emit(Options API)或 emit(Composition API)触发事件时,会立即调用父组件中绑定的事件处理函数,属于同步操作。

示例:

vue

<!-- 子组件 Child.vue -->
<script setup>
const emit = defineEmits(['test']);

console.log('子组件:emit 前');
emit('test');
console.log('子组件:emit 后');
</script>

<!-- 父组件 Parent.vue -->
<Child @test="handleTest" />

<script setup>
function handleTest() {
  console.log('父组件:事件处理中');
}
</script>

输出顺序:

plaintext

子组件:emit 前
父组件:事件处理中
子组件:emit 后

可见 emit 是同步执行的,父组件的事件处理函数会阻塞子组件后续代码。

二、“异步” 表象的场景

1. 结合 v-model 或 update:xxx 事件的更新时机

当 emit 用于更新父组件的响应式数据(如 v-model 的 update:modelValue)时,数据更新是同步的,但DOM 更新是异步的(Vue 的响应式更新机制)。

示例:

vue

<!-- 子组件 -->
<script setup>
const emit = defineEmits(['update:modelValue']);
const update = () => {
  emit('update:modelValue', 'new value');
  console.log('emit 后立即访问父组件数据:', props.modelValue); // 旧值
};
</script>

<!-- 父组件 -->
<Child v-model="value" />
<script setup>
const value = ref('old value');
</script>

emit 触发后,父组件的 value 会同步更新,但子组件的 props.modelValue 由于单向数据流,需等待组件重新渲染后才会更新(异步)。

2. 配合 nextTick 使用

若在 emit 后需要立即操作更新后的 DOM,需通过 nextTick 等待 DOM 异步更新:

javascript

运行

emit('update:modelValue', 'new value');
nextTick(() => {
  console.log('DOM 已更新'); // 异步执行
});
3. 事件处理函数中包含异步操作

若父组件的事件处理函数本身是异步的(如包含 await),则 emit 会等待异步操作完成(但这是函数本身的异步,非 emit 异步):

javascript

运行

// 父组件
async function handleTest() {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('异步处理完成');
}

此时 emit 仍为同步,但会等待异步处理函数执行完毕。

三、自定义异步 emit

若需让 emit 不阻塞子组件代码,可手动用 setTimeout 或 Promise 包装:

javascript

运行

// 子组件
const asyncEmit = () => {
  setTimeout(() => emit('test'), 0); // 异步触发
  console.log('emit 后立即执行'); // 先执行
};

总结

  • emit 本身是同步的:触发事件时会立即执行父组件的处理函数;
  • DOM 更新是异步的:通过 emit 修改数据后,DOM 不会立即更新,需用 nextTick 等待;
  • 事件处理函数可异步:若处理函数包含异步逻辑,emit 会等待其完成(同步等待异步操作)。

日常开发中,无需刻意处理 emit 的同步 / 异步,仅需注意 DOM 更新的异步时机即可。

ts的类型安全

TypeScript 的类型安全(Type Safety)  是指通过静态类型检查机制,在编译阶段捕获类型错误,避免代码在运行时因类型不匹配导致崩溃或异常的特性。它是 TypeScript 最核心的价值,旨在提升代码的可靠性、可维护性,并降低调试成本。

一、TypeScript 类型安全的核心体现

1. 静态类型检查

TypeScript 在编译时对变量、函数、对象等的类型进行校验,而非运行时。一旦发现类型不匹配(如将字符串赋值给数字变量),会直接抛出编译错误,提前拦截问题:

typescript

运行

let age: number = 18;
age = "20"; // ❌ 编译错误:Type 'string' is not assignable to type 'number'
2. 类型推断增强安全性

即使不显式声明类型,TypeScript 会通过类型推断自动推导变量类型,确保类型一致性:

typescript

运行

const name = "Alice"; // 推断为 string 类型
name = 123; // ❌ 编译错误:Type 'number' is not assignable to type 'string'
3. 函数参数 / 返回值的类型约束

通过函数签名明确参数和返回值类型,避免调用时传入错误类型或使用错误的返回值:

typescript

运行

function add(a: number, b: number): number {
  return a + b;
}
add(1, "2"); // ❌ 编译错误:Argument of type 'string' is not assignable to parameter of type 'number'
4. 接口 / 类型别名的结构化类型检查

通过接口或类型别名定义对象结构,确保对象符合预期形状,防止属性缺失或类型错误:

typescript

运行

interface User {
  id: number;
  name: string;
}

function printUser(user: User) {
  console.log(user.id, user.name);
}

printUser({ id: "1", name: "Bob" }); // ❌ 编译错误:Type 'string' is not assignable to type 'number'
5. 联合类型与类型保护的安全处理

联合类型允许变量为多种类型,但需通过类型保护(如 typeof/instanceof)确保操作的类型安全性:

typescript

运行

function formatValue(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // ✅ 安全操作 string 类型
  }
  return value.toFixed(2); // ✅ 安全操作 number 类型
}

二、增强类型安全的关键特性

1. 严格模式(strict: true

开启 tsconfig.json 中的严格模式是保障类型安全的基础,它包含以下子选项:

  • strictNullChecks:禁止将 null/undefined 赋值给非空类型,避免空值访问错误;

    typescript

    运行

    let name: string = null; // ❌ 编译错误(严格模式下)
    
  • noImplicitAny:禁止隐式的 any 类型,强制显式声明类型;

  • strictFunctionTypes:强化函数参数的类型检查,确保函数类型兼容;

  • strictPropertyInitialization:要求类的属性在构造函数中初始化,避免未定义属性。

2. 非空断言与可选链
  • 非空断言(! :明确告诉编译器变量非空(需谨慎使用,避免运行时错误);

    typescript

    运行

    const el = document.getElementById("app")!; // 断言 el 非 null
    
  • 可选链(?. :安全访问嵌套对象属性,避免空值导致的崩溃:

    typescript

    运行

    const userName = user?.profile?.name; // 若 user 或 profile 为 null/undefined,返回 undefined
    
3. 泛型的类型安全复用

泛型允许创建可复用的组件,同时保持类型安全,避免使用 any 丢失类型信息:

typescript

运行

function identity<T>(arg: T): T {
  return arg;
}

const num = identity(123); // 推断为 number 类型
const str = identity("hello"); // 推断为 string 类型
4. 字面量类型与常量断言

字面量类型限制变量为固定值,结合 as const 可将对象 / 数组转为只读常量类型,增强不可变性:

typescript

运行

type Status = "success" | "error";
let status: Status = "success";
status = "loading"; // ❌ 编译错误:Type '"loading"' is not assignable to type 'Status'

const config = { url: "/api" } as const;
config.url = "/new-api"; // ❌ 编译错误:属性 'url' 是只读的
5. 类型守卫与类型收窄

通过类型守卫(如自定义 isXxx 函数)在运行时检查类型,并让编译器推断更具体的类型,避免类型错误:

typescript

运行

interface Cat { meow(): void; }
interface Dog { bark(): void; }

function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}

function handlePet(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow(); // ✅ 安全调用 Cat 方法
  } else {
    animal.bark(); // ✅ 安全调用 Dog 方法
  }
}

三、实际项目中保障类型安全的实践

1. 避免滥用 any

any 会绕过 TypeScript 的类型检查,是类型安全的 “敌人”。若必须使用,优先用 unknown 替代(需显式类型断言才能使用):

typescript

运行

// 不好的写法
let data: any = fetch("/api").then(res => res.json());
data.name; // 无类型检查

// 更好的写法
let data: unknown = fetch("/api").then(res => res.json());
if (typeof data === "object" && data !== null && "name" in data) {
  console.log(data.name); // ✅ 类型安全
}
2. 使用类型工具约束数据结构

通过 ReturnTypePartialReadonly 等工具类型,复用已有类型并约束数据结构,减少重复定义和错误:

typescript

运行

// 提取函数返回值类型
function getUser() {
  return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof getUser>;

// 只读类型
type ReadonlyUser = Readonly<User>;
3. 接口 / 类型别名的精细化设计

为核心业务数据定义清晰的接口,避免 “宽泛” 的类型(如 object),确保每个属性的类型明确:

typescript

运行

// 不好的写法
type Order = object;

// 更好的写法
interface Order {
  id: string;
  amount: number;
  status: "pending" | "paid" | "shipped";
  items: Array<{ productId: string; quantity: number }>;
}
4. 结合 ESLint 强化类型检查

使用 @typescript-eslint 插件的规则(如 no-explicit-anystrict-boolean-expressions),进一步约束代码的类型安全:

json

// .eslintrc.json
{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/strict-boolean-expressions": "error"
  }
}

四、类型安全的局限性

TypeScript 的类型安全是编译时的,运行时类型仍可能因以下原因失效:

  • 第三方库的类型定义不准确;
  • 通过 as 进行不当的类型断言;
  • 与原生 JavaScript 代码交互时的类型丢失;
  • 后端返回数据与前端类型定义不匹配(需结合运行时校验,如使用 zod/joi)。

因此,关键业务场景(如接口数据校验)需结合运行时验证工具,弥补编译时类型检查的不足。

总结

TypeScript 的类型安全通过静态检查、类型推断、严格模式等机制,从编译阶段规避大部分类型错误,大幅提升代码的健壮性。在实际开发中,需遵循 “最小特权原则”—— 尽可能缩小类型范围、避免 any、精细化设计类型结构,才能充分发挥 TypeScript 的类型安全价值。同时,结合运行时校验工具,可进一步覆盖编译时无法触及的场景,构建真正可靠的应用。

ts有哪些类型

TypeScript 的类型安全(Type Safety)  是指通过静态类型检查机制,在编译阶段捕获类型错误,避免代码在运行时因类型不匹配导致崩溃或异常的特性。它是 TypeScript 最核心的价值,旨在提升代码的可靠性、可维护性,并降低调试成本。

一、TypeScript 类型安全的核心体现

1. 静态类型检查

TypeScript 在编译时对变量、函数、对象等的类型进行校验,而非运行时。一旦发现类型不匹配(如将字符串赋值给数字变量),会直接抛出编译错误,提前拦截问题:

typescript

运行

let age: number = 18;
age = "20"; // ❌ 编译错误:Type 'string' is not assignable to type 'number'
2. 类型推断增强安全性

即使不显式声明类型,TypeScript 会通过类型推断自动推导变量类型,确保类型一致性:

typescript

运行

const name = "Alice"; // 推断为 string 类型
name = 123; // ❌ 编译错误:Type 'number' is not assignable to type 'string'
3. 函数参数 / 返回值的类型约束

通过函数签名明确参数和返回值类型,避免调用时传入错误类型或使用错误的返回值:

typescript

运行

function add(a: number, b: number): number {
  return a + b;
}
add(1, "2"); // ❌ 编译错误:Argument of type 'string' is not assignable to parameter of type 'number'
4. 接口 / 类型别名的结构化类型检查

通过接口或类型别名定义对象结构,确保对象符合预期形状,防止属性缺失或类型错误:

typescript

运行

interface User {
  id: number;
  name: string;
}

function printUser(user: User) {
  console.log(user.id, user.name);
}

printUser({ id: "1", name: "Bob" }); // ❌ 编译错误:Type 'string' is not assignable to type 'number'
5. 联合类型与类型保护的安全处理

联合类型允许变量为多种类型,但需通过类型保护(如 typeof/instanceof)确保操作的类型安全性:

typescript

运行

function formatValue(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase(); // ✅ 安全操作 string 类型
  }
  return value.toFixed(2); // ✅ 安全操作 number 类型
}

二、增强类型安全的关键特性

1. 严格模式(strict: true

开启 tsconfig.json 中的严格模式是保障类型安全的基础,它包含以下子选项:

  • strictNullChecks:禁止将 null/undefined 赋值给非空类型,避免空值访问错误;

    typescript

    运行

    let name: string = null; // ❌ 编译错误(严格模式下)
    
  • noImplicitAny:禁止隐式的 any 类型,强制显式声明类型;

  • strictFunctionTypes:强化函数参数的类型检查,确保函数类型兼容;

  • strictPropertyInitialization:要求类的属性在构造函数中初始化,避免未定义属性。

2. 非空断言与可选链
  • 非空断言(! :明确告诉编译器变量非空(需谨慎使用,避免运行时错误);

    typescript

    运行

    const el = document.getElementById("app")!; // 断言 el 非 null
    
  • 可选链(?. :安全访问嵌套对象属性,避免空值导致的崩溃:

    typescript

    运行

    const userName = user?.profile?.name; // 若 user 或 profile 为 null/undefined,返回 undefined
    
3. 泛型的类型安全复用

泛型允许创建可复用的组件,同时保持类型安全,避免使用 any 丢失类型信息:

typescript

运行

function identity<T>(arg: T): T {
  return arg;
}

const num = identity(123); // 推断为 number 类型
const str = identity("hello"); // 推断为 string 类型
4. 字面量类型与常量断言

字面量类型限制变量为固定值,结合 as const 可将对象 / 数组转为只读常量类型,增强不可变性:

typescript

运行

type Status = "success" | "error";
let status: Status = "success";
status = "loading"; // ❌ 编译错误:Type '"loading"' is not assignable to type 'Status'

const config = { url: "/api" } as const;
config.url = "/new-api"; // ❌ 编译错误:属性 'url' 是只读的
5. 类型守卫与类型收窄

通过类型守卫(如自定义 isXxx 函数)在运行时检查类型,并让编译器推断更具体的类型,避免类型错误:

typescript

运行

interface Cat { meow(): void; }
interface Dog { bark(): void; }

function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}

function handlePet(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow(); // ✅ 安全调用 Cat 方法
  } else {
    animal.bark(); // ✅ 安全调用 Dog 方法
  }
}

三、实际项目中保障类型安全的实践

1. 避免滥用 any

any 会绕过 TypeScript 的类型检查,是类型安全的 “敌人”。若必须使用,优先用 unknown 替代(需显式类型断言才能使用):

typescript

运行

// 不好的写法
let data: any = fetch("/api").then(res => res.json());
data.name; // 无类型检查

// 更好的写法
let data: unknown = fetch("/api").then(res => res.json());
if (typeof data === "object" && data !== null && "name" in data) {
  console.log(data.name); // ✅ 类型安全
}
2. 使用类型工具约束数据结构

通过 ReturnTypePartialReadonly 等工具类型,复用已有类型并约束数据结构,减少重复定义和错误:

typescript

运行

// 提取函数返回值类型
function getUser() {
  return { id: 1, name: "Alice" };
}
type User = ReturnType<typeof getUser>;

// 只读类型
type ReadonlyUser = Readonly<User>;
3. 接口 / 类型别名的精细化设计

为核心业务数据定义清晰的接口,避免 “宽泛” 的类型(如 object),确保每个属性的类型明确:

typescript

运行

// 不好的写法
type Order = object;

// 更好的写法
interface Order {
  id: string;
  amount: number;
  status: "pending" | "paid" | "shipped";
  items: Array<{ productId: string; quantity: number }>;
}
4. 结合 ESLint 强化类型检查

使用 @typescript-eslint 插件的规则(如 no-explicit-anystrict-boolean-expressions),进一步约束代码的类型安全:

json

// .eslintrc.json
{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/strict-boolean-expressions": "error"
  }
}

四、类型安全的局限性

TypeScript 的类型安全是编译时的,运行时类型仍可能因以下原因失效:

  • 第三方库的类型定义不准确;
  • 通过 as 进行不当的类型断言;
  • 与原生 JavaScript 代码交互时的类型丢失;
  • 后端返回数据与前端类型定义不匹配(需结合运行时校验,如使用 zod/joi)。

因此,关键业务场景(如接口数据校验)需结合运行时验证工具,弥补编译时类型检查的不足。

总结

TypeScript 的类型安全通过静态检查、类型推断、严格模式等机制,从编译阶段规避大部分类型错误,大幅提升代码的健壮性。在实际开发中,需遵循 “最小特权原则”—— 尽可能缩小类型范围、避免 any、精细化设计类型结构,才能充分发挥 TypeScript 的类型安全价值。同时,结合运行时校验工具,可进一步覆盖编译时无法触及的场景,构建真正可靠的应用。

用户点击某个按钮的时候发生了什么

用户点击某个按钮时,会触发从浏览器底层事件机制前端框架逻辑的一系列连锁反应,涉及 DOM 事件、JavaScript 执行、框架处理等多个层面。以下从原生浏览器行为到框架(如 Vue/React)处理的完整流程拆解:

一、浏览器底层的事件触发流程

1. 物理交互与事件初始化

用户点击按钮时,操作系统捕获鼠标 / 触摸硬件的信号,传递给浏览器。浏览器将其转化为DOM 事件(如 click),并初始化事件对象(MouseEvent),包含事件类型、目标元素、坐标、修饰键(如 Ctrl)等信息。

2. 事件传播(捕获 → 目标 → 冒泡)

DOM 事件遵循捕获 - 目标 - 冒泡的传播机制:

  • 捕获阶段:事件从 window 向下传递到目标元素的父级,最终到达目标按钮(可通过 addEventListener(type, handler, { capture: true }) 监听);
  • 目标阶段:触发按钮本身绑定的 click 事件处理函数;
  • 冒泡阶段:事件从目标按钮向上冒泡回 window(可通过 event.stopPropagation() 阻止)。
3. 浏览器默认行为

若按钮是原生控件(如 <button type="submit">),点击后会触发浏览器默认行为(如提交表单),可通过 event.preventDefault() 阻止。

二、原生 JavaScript 中的处理逻辑

若按钮绑定了原生事件处理函数:

javascript

运行

button.addEventListener('click', (e) => {
  // 1. 事件对象操作(阻止冒泡/默认行为)
  e.stopPropagation();
  e.preventDefault();

  // 2. 业务逻辑执行(同步/异步)
  console.log('按钮被点击');
  fetch('/api/data').then(res => res.json()); // 异步操作

  // 3. DOM 操作(如修改样式/内容)
  button.textContent = '已点击';
});

此时执行顺序为:事件回调同步执行 → 若有异步操作(如网络请求)则进入任务队列 → 同步代码执行完毕后,浏览器渲染 DOM 更新 → 异步任务完成后触发后续逻辑。

三、Vue 中的按钮点击处理

1. 模板绑定与事件解析

Vue 模板中的 @click="handleClick" 会被编译器解析为事件监听,最终通过 addEventListener 绑定到按钮元素。

2. 响应式更新与 DOM 渲染

若点击处理函数中修改了响应式数据(如 ref/reactive):

vue

<script setup>
const count = ref(0);
const handleClick = () => {
  count.value++; // 修改响应式数据
  console.log('count:', count.value); // 同步更新
};
</script>

<template>
  <button @click="handleClick">点击 {{ count }}</button>
</template>
  • 数据修改会触发 Vue 的依赖收集,标记相关组件为 “脏组件”;
  • Vue 将 DOM 更新操作推入异步队列,等待当前同步代码执行完毕后,批量更新 DOM(通过 nextTick 实现);
  • 最终按钮上的 count 文本会异步更新。
3. 事件修饰符的处理

Vue 提供的事件修饰符(如 @click.stop@click.prevent)会被编译为对应的原生事件操作(如 e.stopPropagation()),简化代码。

四、React 中的按钮点击处理

1. 合成事件(SyntheticEvent)

React 不会直接绑定原生事件,而是通过事件委托将所有事件监听绑定到 document,触发时生成合成事件对象(兼容各浏览器),再分发到目标组件的事件处理函数。

2. 状态更新与组件重渲染

jsx

function Button() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(prev => prev + 1); // 异步状态更新
    console.log('count:', count); // 输出旧值(状态更新异步)
    
    // 若需获取更新后的值,使用 useEffect
    useEffect(() => {
      console.log('更新后的count:', count);
    }, [count]);
  };

  return <button onClick={handleClick}>点击 {count}</button>;
}
  • React 的 setState/setCount 是异步的,多次调用会被批量处理;
  • 状态更新后,组件会触发重渲染,生成新的虚拟 DOM,与旧虚拟 DOM 对比(Diffing)后,更新真实 DOM。

五、通用的后续行为

无论原生还是框架场景,按钮点击后还可能触发:

  • 样式变化:如:active/:hover 伪类生效,或通过 JS 修改 class/style;
  • 动画 / 过渡:CSS 动画或 JS 驱动的动画执行;
  • 路由跳转:若按钮绑定了路由跳转逻辑(如 Vue Router 的 $router.push);
  • 数据请求:调用后端接口获取 / 提交数据;
  • 状态管理更新:修改全局状态(如 Vuex/Pinia/Redux),触发其他组件的更新。

总结

用户点击按钮的核心流程可概括为:硬件交互 → 浏览器事件初始化 → 事件传播 → 框架 / JS 处理逻辑(数据更新 / 业务操作) → DOM/UI 渲染 → 后续副作用(请求 / 跳转等)

框架(Vue/React)在此基础上封装了事件机制和响应式更新逻辑,简化开发的同时,保证了性能优化(如批量更新、虚拟 DOM)。理解这一流程有助于排查事件相关的 bug(如冒泡冲突、异步更新时机)。

不同标签页如何通信

浏览器中不同标签页(或窗口)之间的通信,本质是利用浏览器提供的全局存储或通信机制实现跨上下文数据交互。以下是常见的实现方式、原理及适用场景:

一、localStorage + storage 事件

原理
  • localStorage 是浏览器同源策略下的本地存储,所有同源标签页共享该存储;
  • 当某标签页修改 localStorage 时,其他同源标签页会触发 window 的 storage 事件(当前修改页不触发)。
实现示例

javascript

运行

// 发送消息的标签页
localStorage.setItem('cross-tab-message', JSON.stringify({ text: 'Hello from Tab 1' }));

// 接收消息的标签页
window.addEventListener('storage', (e) => {
  if (e.key === 'cross-tab-message') {
    const message = JSON.parse(e.newValue);
    console.log('收到消息:', message.text); // Hello from Tab 1
  }
});
特点
  • ✅ 简单易用,兼容性好(支持所有现代浏览器);
  • ❌ 仅支持字符串数据(需序列化 / 反序列化);
  • ❌ 无法主动发送消息,需依赖存储修改触发;
  • ❌ 同源限制(协议、域名、端口需一致)。

二、BroadcastChannel API

原理

BroadcastChannel 是浏览器专门为跨上下文通信设计的 API,允许同源标签页创建命名通道,通过该通道发送 / 接收消息。

实现示例

javascript

运行

// 标签页 A
const channel = new BroadcastChannel('my-channel');
channel.postMessage({ text: 'Hello from Tab A' });

// 标签页 B
const channel = new BroadcastChannel('my-channel');
channel.onmessage = (e) => {
  console.log('收到消息:', e.data.text); // Hello from Tab A
};

// 关闭通道(可选)
channel.close();
特点
  • ✅ 支持任意结构化数据(对象、数组等,无需序列化);
  • ✅ 主动发送消息,实时性强;
  • ✅ 同源限制,安全性高;
  • ❌ 兼容性:IE 不支持,现代浏览器均支持(Chrome 54+/Firefox 38+)。

三、SharedWorker 共享工作线程

原理

SharedWorker 是所有同源标签页共享的后台线程,标签页通过与该线程通信,实现间接的跨标签页数据交互。

实现示例

javascript

运行

// 共享工作线程(shared-worker.js)
let ports = [];
self.onconnect = (e) => {
  const port = e.ports[0];
  ports.push(port);
  
  port.onmessage = (e) => {
    // 转发消息给所有连接的标签页
    ports.forEach(p => p.postMessage(e.data));
  };
};

// 标签页 A/B
const worker = new SharedWorker('shared-worker.js');
worker.port.start();

// 发送消息
worker.port.postMessage({ text: 'Hello from Tab' });

// 接收消息
worker.port.onmessage = (e) => {
  console.log('收到消息:', e.data.text);
};
特点
  • ✅ 支持复杂通信逻辑(如消息转发、状态共享);
  • ✅ 同源限制,适合长期通信;
  • ❌ 实现较复杂,需单独编写 Worker 脚本;
  • ❌ 兼容性一般(IE 不支持,部分移动端浏览器有限支持)。

四、window.postMessage(跨域 / 同域)

原理

postMessage 允许不同窗口(包括跨域)通过引用直接通信,需获取目标窗口的引用(如通过 window.open 或 iframe 的 contentWindow)。

实现示例

javascript

运行

// 父标签页打开新标签页
const newTab = window.open('https://example.com');

// 父标签页发送消息
newTab.postMessage({ text: 'Hello from Parent' }, 'https://example.com');

// 新标签页接收消息
window.addEventListener('message', (e) => {
  // 验证消息来源(重要,防止跨域攻击)
  if (e.origin !== 'https://parent-domain.com') return;
  console.log('收到消息:', e.data.text);
});
特点
  • ✅ 支持跨域通信(需验证 origin);
  • ✅ 直接通信,实时性高;
  • ❌ 需获取目标窗口引用(无法用于无关联的标签页);
  • ❌ 跨域时需严格验证来源,避免安全风险。

五、IndexedDB(进阶场景)

原理

IndexedDB 是浏览器的异步数据库,同源标签页可共享数据库,通过监听数据库变化实现通信(需结合 onversionchange 或轮询)。

适用场景
  • 需共享大量结构化数据;
  • 需持久化存储通信内容。
特点
  • ✅ 支持复杂数据结构,存储容量大;
  • ❌ 实现复杂,适合进阶场景;
  • ❌ 实时性依赖监听或轮询。

六、服务端中转(WebSocket/SSE/HTTP)

原理

通过后端服务作为中介,标签页分别与服务端建立连接,实现间接通信:

  • WebSocket:全双工通信,服务端转发消息给所有连接的客户端;
  • SSE(Server-Sent Events) :服务端单向推送消息;
  • HTTP 轮询 / 长轮询:定期请求服务端获取最新数据。
实现示例(WebSocket)

javascript

运行

// 标签页 A/B 连接 WebSocket 服务
const ws = new WebSocket('wss://example.com/ws');

// 发送消息
ws.send(JSON.stringify({ text: 'Hello from Tab' }));

// 接收服务端转发的消息
ws.onmessage = (e) => {
  const message = JSON.parse(e.data);
  console.log('收到消息:', message.text);
};
特点
  • ✅ 支持跨域、跨设备通信;
  • ✅ 实时性高,适合大规模通信;
  • ❌ 依赖后端服务,增加服务器成本;
  • ❌ 需处理连接维护、断线重连等逻辑。

各方案对比与选型建议

方案同源限制实时性复杂度适用场景
localStorage简单消息通知、小型数据共享
BroadcastChannel同源标签页实时通信(推荐)
SharedWorker复杂通信逻辑、状态共享
postMessage有窗口引用的跨域 / 同域通信
IndexedDB大量结构化数据共享 + 持久化
服务端中转跨域 / 跨设备、大规模通信

安全注意事项

  1. 同源策略:优先使用同源通信方案,跨域时严格验证 origin
  2. 数据校验:接收消息时校验数据格式,避免注入攻击;
  3. 敏感信息:避免通过本地存储传输敏感数据(如 token),优先用服务端中转并加密。

根据场景选择合适的方案,简单同源通信推荐 BroadcastChannel,跨域或大规模通信推荐服务端中转。

foreach 、for in 、for of区别

forEachfor...infor...of 是 JavaScript 中三种常见的遍历方式,它们在遍历对象、数组的能力、遍历机制、适用场景等方面存在显著差异。以下是详细对比:

一、forEach

1. 本质与适用对象

forEach 是数组的原型方法Array.prototype.forEach),仅适用于数组(或类数组对象需通过 Array.from 转换),用于遍历数组元素。

2. 语法

javascript

运行

array.forEach((element, index, array) => {
  // 遍历逻辑
});
  • element:当前遍历的元素;
  • index:当前元素的索引;
  • array:原数组。
3. 特点
  • 无法中断遍历:不能用 break/continue 终止或跳过循环,只能通过 return 跳过当前迭代;
  • 无返回值:返回值始终为 undefined,无法链式调用(如 forEach().map() 不合法);
  • 遍历数组元素:仅遍历数组的数值索引属性(忽略非索引属性);
  • 异步不阻塞:若回调中有异步操作(如 await),forEach 不会等待,会同步执行完所有回调。
示例

javascript

运行

const arr = [1, 2, 3];
arr.forEach((item, index) => {
  console.log(index, item); // 0 1 → 1 2 → 2 3
});

// 无法中断
arr.forEach(item => {
  if (item === 2) break; // ❌ 报错:Illegal break statement
});

二、for...in

1. 本质与适用对象

for...in 是遍历对象可枚举属性的语句,适用于任意对象(包括数组、普通对象、原型链属性)。

2. 语法

javascript

运行

for (const key in object) {
  // 遍历逻辑
}
  • key:对象的属性名(数组中为索引字符串,如 "0""1")。
3. 特点
  • 遍历所有可枚举属性:包括对象自身的属性和原型链上继承的可枚举属性(需用 hasOwnProperty 过滤);
  • 遍历顺序不确定:普通对象的属性遍历顺序不保证(数组索引会按数值顺序遍历,但不推荐用于数组);
  • 数组慎用:遍历数组时,key 是字符串类型的索引,且可能遍历到数组的非索引属性(如 arr.name = "test" 会被遍历);
  • 可中断:支持 break/continue
示例

javascript

运行

const obj = { a: 1, b: 2 };
for (const key in obj) {
  console.log(key, obj[key]); // a 1 → b 2
}

const arr = [1, 2, 3];
arr.name = "myArr";
for (const key in arr) {
  console.log(key); // 0 → 1 → 2 → name(遍历到非索引属性)
}

// 过滤原型链属性
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key); // 仅遍历自身属性
  }
}

三、for...of

1. 本质与适用对象

for...of 是遍历可迭代对象(Iterable)  的语句,适用于实现了 Symbol.iterator 接口的对象,包括:

  • 数组、字符串、Set、Map;
  • 生成器(Generator);
  • DOM 集合(如 NodeList)。
2. 语法

javascript

运行

for (const value of iterable) {
  // 遍历逻辑
}
  • value:可迭代对象的当前元素值。
3. 特点
  • 遍历值而非键:直接获取元素值(数组中为元素,Map 中为 [key, value] 数组);
  • 支持中断:可使用 break/continue/return 终止或跳过循环;
  • 异步遍历:可结合 for await...of 遍历异步可迭代对象(如异步生成器);
  • 不遍历原型链:仅遍历对象自身的可迭代元素;
  • 普通对象不可用:普通对象默认未实现 Symbol.iterator,需手动实现。
示例

javascript

运行

// 遍历数组
const arr = [1, 2, 3];
for (const item of arr) {
  if (item === 2) break; // ✅ 中断
  console.log(item); // 1
}

// 遍历 Map
const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
  console.log(key, value); // a 1 → b 2
}

// 异步遍历
async function asyncIter() {
  const asyncArr = [Promise.resolve(1), Promise.resolve(2)];
  for await (const item of asyncArr) {
    console.log(item); // 1 → 2
  }
}

四、核心区别对比表

特性forEachfor...infor...of
适用对象数组(类数组需转换)任意对象(数组 / 普通对象)可迭代对象(数组 / Set/Map 等)
遍历内容数组元素 + 索引对象属性名(字符串)可迭代对象的元素值
是否可中断❌(仅 return 跳过当前)✅(break/continue✅(break/continue
原型链属性不遍历遍历(需手动过滤)不遍历
异步支持❌(不等待异步操作)✅(for await...of
返回值undefined
数组非索引属性不遍历遍历不遍历

五、选型建议

  1. 遍历数组且无需中断:用 forEach(简洁);
  2. 遍历数组且需中断 / 异步:用 for...of(灵活);
  3. 遍历普通对象属性:用 for...in(需配合 hasOwnProperty);
  4. 遍历 Set/Map/ 字符串:用 for...of(原生支持);
  5. 性能要求高的场景:优先用普通 for 循环(for (let i=0; i<arr.length; i++)),比三者更快。

例如:

  • 简单遍历数组元素:forEach
  • 遍历数组并可能中断:for...of
  • 遍历对象的自身属性:for...in + hasOwnProperty
  • 遍历 Map 键值对:for...of