Vue3,Pinia与TypeScript面试题总结

Vue3、Pinia与TypeScript面试题汇总

在Pinia中如何定义带有类型推断的store?

使用 defineStore 的选项式语法,Pinia 会自动推断类型,但可以显式 定义类型以增强类型安全。

	import {
   
    defineStore } from 'pinia';
	
	// 1. 定义 State 的接口(可选,但推荐)
	interface UserStoreState {
   
   
	  users: User[];
	  currentPage: number;
	  isLoading: boolean;
	}
	
	// 2. 定义 Store
	export const useUserStore = defineStore('user', {
   
   
	  // State(显式标注类型)
	  state: (): UserStoreState => ({
   
   
	    users: [],
	    currentPage: 1,
	    isLoading: false,
	  }),
	
	  // Getters(自动推断返回类型)
	  getters: {
   
   
	    // 示例:过滤用户
	    activeUsers: (state) => state.users.filter(user => user.isActive),
	    // 带参数的类型标注
	    getUserById: (state) => {
   
   
	      return (userId: string) => state.users.find(user => user.id === userId);
	    },
	  },
	
	  // Actions(显式标注参数和返回类型)
	  actions: {
   
   
	    async fetchUsers() {
   
   
	      this.isLoading = true;
	      try {
   
   
	        const response = await api.get<User[]>(`/users?page=${
     
     this.currentPage}`);
	        this.users = response.data;
	      } catch (error) {
   
   
	        console.error('Failed to fetch users:', error);
	      } finally {
   
   
	        this.isLoading = false;
	      }
	    },
	    setPage(page: number) {
   
   
	      this.currentPage = page;
	    },
	  },
	});

使用 storeToRefs 保持响应式
在组件中解构 Store 时保留响应式。

import {
   
    storeToRefs } from 'pinia';
const store = useUserStore();
const {
   
    users, currentPage } = storeToRefs(store); // 保持响应式

Vue的响应式原理(Vue 2和Vue 3的区别)

Vue的响应式原理是其数据驱动视图的核心机制,Vue 2和Vue 3在实现方式上有显著差异,主要体现在底层依赖的API和对数据变化的检测能力上:


Vue 2 的响应式原理

实现方式:
Vue 2 使用 Object.defineProperty 对对象的属性进行劫持,将其转化为 gettersetter

  1. 依赖收集(Getter):
    当组件渲染时访问数据属性,触发 getter,将当前的 Watcher(依赖)收集到依赖列表中(Dep)。

  2. 触发更新(Setter):
    当数据被修改时,触发 setter,通知所有依赖的 Watcher 更新视图。

局限性:

  • 无法检测对象属性的新增或删除
    必须通过 Vue.setVue.delete 显式操作才能触发响应式更新。
  • 对数组的监听受限
    直接通过索引修改数组(如 arr[0] = 1)或修改数组长度(如 arr.length = 0)无法触发更新。
    Vue 2 通过重写数组的 pushpopsplice 等变异方法(Mutation Methods)来支持数组响应式。

示例:

// Vue 2 数据初始化
const data = {
   
    count: 0 };
Object.defineProperty(data, 'count', {
   
   
  get() {
   
   
    // 收集依赖
    dep.depend();
    return value;
  },
  set(newVal) {
   
   
    value = newVal;
    // 触发更新
    dep.notify();
  }
});

Vue 3 的响应式原理

实现方式:
Vue 3 改用 Proxy 代理整个对象,结合 Reflect 实现更全面的拦截能力。

  1. Proxy 的拦截操作
    Proxy 可以拦截对象的 getsetdeleteProperty 等操作,无需逐个劫持属性。

  2. 深层响应式
    通过递归代理嵌套对象,实现深层次的响应式跟踪。

优势:

  • 支持动态新增/删除属性
    直接操作 obj.newKey = valuedelete obj.key 也能触发响应式。
  • 原生支持数组和复杂数据结构
    无需重写数组方法,直接通过索引修改或调用 arr.length 即可触发更新。
  • 性能优化
    Proxy 仅在访问属性时递归代理,避免初始化时深度遍历所有属性。

示例:

// Vue 3 数据初始化
const data = {
   
    count: 0 };
const proxy = new Proxy(data, {
   
   
  get(target, key, receiver) {
   
   
    track(target, key); // 收集依赖
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
   
   
    Reflect.set(target, key, value, receiver);
    trigger(target, key); // 触发更新
    return true;
  }
});

关键区别对比

特性 Vue 2(Object.defineProperty) Vue 3(Proxy)
属性监听范围 仅能劫持已存在的属性 支持动态新增、删除属性
数组监听 需重写数组方法(push/pop等) 原生支持索引修改和length变化
深层嵌套对象 初始化时递归遍历所有属性 按需代理(访问时才递归)
兼容性 支持 IE9+ 不支持 IE(需 Polyfill)
性能 初始化时递归劫持,大型对象性能较差 延迟代理,内存和初始化性能更优
复杂数据结构支持 不支持 Map、Set、WeakMap 等 支持

v-if 和 v-show 的区别是什么?

核心区别

特性 v-if v-show
实现方式 动态添加/移除DOM元素 通过CSS display切换显示状态
初始渲染 条件为false不渲染元素 无论条件如何,始终渲染元素
切换性能 较高(涉及DOM操作) 较低(仅修改CSS属性)
适用场景 条件不频繁切换且可能为false 条件频繁切换
生命周期 触发组件的创建/销毁生命周期钩子 不触发生命周期,仅切换显示
v-else配合 支持逻辑分支(v-else/v-else-if 不支持
性能影响
  • v-if

    • 初始渲染开销低:若初始条件为false,无需渲染元素。
    • 切换开销高:频繁切换时,频繁的DOM操作会降低性能。
  • v-show

    • 初始渲染开销高:无论条件如何,元素都会被渲染。
    • 切换开销低:仅修改CSS属性,适合高频切换场景(如选项卡)。
生命周期行为
  • v-if
    条件首次为true时触发组件的createdmounted钩子;条件变为false时触发unmounted钩子。

  • v-show
    无论条件如何变化,组件的生命周期钩子不会触发,元素始终存在。


Vue组件的生命周期钩子有哪些?哪个阶段适合发起数据请求?

Vue 生命周期钩子(以 Vue 2 为主,兼容 Vue 3)

1. 创建阶段(Initialization)
  • beforeCreate

    • 触发时机:实例初始化后,数据观测(data)和事件配置(methods)之前。
    • 用途:通常用于插件初始化(如 Vuex action 订阅),但此时无法访问组件数据。
  • created

    • 触发时机:实例创建完成,数据观测、计算属性、方法已配置,但 DOM 未生成。
    • 用途适合发起异步请求(如 API 调用)、初始化非 DOM 相关数据。
2. 挂载阶段(Mounting)
  • beforeMount

    • 触发时机:模板编译完成,但尚未将虚拟 DOM 渲染为真实 DOM。
    • 用途:极少使用,适用于需要在挂载前修改模板的场景。
  • mounted

    • 触发时机:DOM 已挂载,可以访问 this.$el
    • 用途适合操作 DOM(如初始化图表库)、依赖 DOM 的第三方库初始化。
3. 更新阶段(Updating)
  • beforeUpdate

    • 触发时机:数据变化后,虚拟 DOM 重新渲染前。
    • 用途:获取更新前的 DOM 状态(如滚动位置)。
  • updated

    • 触发时机:虚拟 DOM 重新渲染并应用到真实 DOM 后。
    • 用途:执行依赖最新 DOM 的操作(如调整元素尺寸),但避免在此处修改数据(可能导致无限循环)。
4. 销毁阶段
  • beforeDestroy(Vue 2)/ beforeUnmount(Vue 3)

    • 触发时机:实例销毁前,组件仍完全可用。
    • 用途清理定时器、取消事件监听、释放外部资源(如 WebSocket 连接)。
  • destroyed(Vue 2)/ unmounted(Vue 3)

    • 触发时机:实例销毁后,所有子组件也已被销毁。
    • 用途:极少使用,适用于最终清理逻辑。
5. 缓存组件生命周期(Keep-Alive)
  • activated

    • 触发时机:被 <keep-alive> 缓存的组件重新激活时(如切换回该组件)。
    • 用途:恢复组件状态(如重新请求数据)。
  • deactivated

    • 触发时机:被 <keep-alive> 缓存的组件停用时(如切换到其他组件)。
    • 用途:保存组件状态(如表单输入内容)。

适合发起数据请求的阶段

1. created 阶段
  • 推荐场景
    • 需要尽早获取数据(减少用户等待时间)。
    • 不依赖 DOM 的操作(如纯数据初始化)。
  • 示例
    created() {
         
         
      this.fetchUserData(); // 初始化用户数据
    }
    
2. mounted 阶段
  • 推荐场景
    • 需要操作 DOM 或依赖 DOM 的库(如地图、图表初始化)。
    • 需要获取元素尺寸或位置。
  • 示例
    mounted() {
         
         
      this.initChart(); // 依赖 DOM 的图表库初始化
    }
    
3. activated 阶段(配合 <keep-alive>
  • 推荐场景
    • 缓存组件需要动态更新数据(如切换回页面时刷新列表)。
  • 示例
    activated() {
         
         
      this.refreshData(); // 重新请求最新数据
    }
    

组件间通信方式有哪些?

  • 注意:Vue 3 中需改用第三方库(如 mitt)。

** 复杂场景通信**

方式:状态管理库(Vuex/Pinia)
  • Vuex(Vue 2 官方方案):

    • 集中式状态管理,通过 statemutationsactions 管理数据流。
    • 示例
      // 组件中获取状态
      this.$store.state.count;
      
      // 触发 Action
      this.$store.dispatch('fetchData');
      
  • Pinia(Vue 3 推荐方案):

    • 更简洁的 API,支持 TypeScript。
    • 示例
      // 定义 Store
      export const useStore = defineStore('main', {
             
             
        state: () => ({
             
              count: 0 }),
        actions: {
             
              increment() {
             
              this.count++ } }
      });
      
      // 组件中使用
      const store = useStore();
      store.count++;
      

** 直接访问组件实例**

方式:$parent / $children
  • 用法:通过组件实例链直接访问父子组件。
  • 缺点:增加耦合,不利于维护,慎用。
方式:$refs
  • 用法:父组件通过 ref 属性获取子组件实例。
  • 示例
    <!-- 父组件 -->
    <Child ref="childRef" />
    <script>
    export default {
      mounted() {
        this.$refs.childRef.doSomething();
      }
    }
    </script>
    

** 其他方式**

方式:浏览器存储(LocalStorage/SessionStorage)
  • 通过浏览器存储共享数据,需手动监听 storage 事件。
  • 适用场景:持久化数据(如用户登录状态)。
方式:作用域插槽(Scoped Slots)
  • 父组件通过插槽向子组件传递模板,子组件通过插槽prop暴露数据。
  • 示例
    <!-- 子组件 -->
    <slot :data="childData"></slot>
    
    <!-- 父组件 -->
    <Child>
      <template v-slot="slotProps">
        {
        
        { slotProps.data }}
      </template>
    </Child>
    

如何选择通信方式?

场景 推荐方式
父子组件简单通信 Props / $emit
兄弟组件通信 事件总线 / 状态管理库
跨层级组件共享数据 Provide/Inject
复杂应用状态管理 Vuex(Vue 2) / Pinia(Vue 3)
需要直接操作子组件 $refs
非响应式数据或简单全局配置 事件总线 / 浏览器存储

interface 和 type 的区别是什么?

核心区别

特性 interface type(类型别名)
声明合并 ✅ 支持(同名接口自动合并) ❌ 不支持
扩展方式 通过 extends 继承 通过 &(交叉类型)组合
适用类型范围 主要用于对象类型 支持更广泛的类型(联合、元组、字面量等)
实现(implements) 类可以直接实现接口 类不能直接实现类型别名(除非是对象类型)
工具类型兼容性 完全支持(如 Partial<Interface> 完全支持(如 Partial<Type>
声明合并(Declaration Merging)
  • interface:允许多次声明同名接口,TypeScript 会自动合并成员。

    interface User {
         
         
      name: string;
    }
    interface User {
         
         
      age: number;
    }
    
    // 最终 User 接口包含 name 和 age
    const user: User = {
         
          name: "Alice", age: 25 };
    
  • type:同名类型别名会报错(重复定义)。

    type User = {
         
          name: string };  // ✅
    type User = {
         
          age: number };   // ❌ Error: Duplicate identifier 'User'
    
** 类型扩展**
  • interface:通过 extends 继承其他接口。
    interface Animal {
         
         
      name: string;
    }
    
    interface Dog extends Animal {
         
         
      bark(): void;
    }
    
** 支持的类型范围**
  • interface:主要用于定义对象类型

    interface Point {
         
         
      x: number;
      y: number;
    }
    
  • type:支持更复杂的类型定义:

    • 联合类型type Status = "success" | "error";
    • 元组类型type Coordinates = [number, number];
    • 函数类型type Handler = (input: string) => void;
    • 映射类型type ReadonlyUser = Readonly<User>;
    • 基本类型别名type ID = string | number;

使用场景建议

优先使用 interface 的情况
  • 定义对象类型:尤其是需要扩展或合并的场景(如库的类型定义)。
  • 类实例的契约:明确类需要实现的属性和方法。
  • 利用声明合并:扩展第三方库的类型(如为 Window 添加自定义属性)。
优先使用 type 的情况
  • 复杂类型组合:联合类型、交叉类型、元组等。
    type Result<T> = {
         
          data: T } | {
         
          error: string };
    
  • 字面量类型:定义明确的枚举值集合。
    type Direction = "up" | "down" | "left" | "right";
    
  • 工具类型操作:基于现有类型生成新类型。
    type PartialUser = Partial<User>;
    

    示例对比

定义函数类型
  • interface

    interface SearchFunc {
         
         
      (source: string, keyword: string): boolean;
    }
    
  • type

    type SearchFunc = (source: string, keyword: string) => boolean;
    
联合类型(只能用 type
type UserID = string | number;

function getUser(id: UserID) {
   
    ... }

TS中的泛型是什么?举例说明在函数中的应用。

在函数中使用泛型的例子

假设我们想要编写一个函数,该函数返回传入的参数本身。如果我们不使用泛型,我们可能需要为每种类型都写一个这样的函数,或者让函数接受并返回any类型,但这会失去类型检查的好处。使用泛型,我们可以这样写:

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

在这个例子中,T是一个类型变量,它代表了一个未指定的类型。当我们调用identity函数时,可以显式地指定T的具体类型,或者让TypeScript根据传入的参数自动推断出类型。

调用示例
let output = identity<string>("myString");
console.log(output); // 输出: "myString"

// 或者让TypeScript自动推断类型
output = identity("myString"); // TypeScript会自动推断T为string
console.log(output); // 输出: "myString"

带有约束的泛型

有时候,你可能希望对泛型的类型进行一些限制,只允许某些类型的值被传递。这时,你可以使用泛型约束。例如,如果你只想允许对象类型的值,可以这样做:

interface Lengthwise {
   
   
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
   
   
    console.log(arg.length);
    return arg;
}

loggingIdentity({
   
   length: 10, value: 'hello'}); // 正确
// loggingIdentity(37); // 错误,数字没有length属性

类型断言(as)和非空断言(!)的使用场景及风险

** 类型断言(as)**

使用场景
  1. 明确知道值的实际类型
    当开发者比编译器更清楚值的类型时,强制指定类型。

    // 从外部数据源获取的值
    const data = JSON.parse('{ "name": "Alice" }') as {
         
          name: string };
    
  2. 处理联合类型的窄化
    在无法通过类型守卫(typeof/instanceof)缩小类型范围时,手动断言。

    const element = document.getElementById("input") as HTMLInputElement;
    
  3. 兼容旧代码或第三方库
    处理类型定义不完整的第三方库返回值。

    const value = (window as any).externalValue as string;
    
风险
  • 掩盖类型错误
    若断言类型与实际类型不符,编译器不会报错,但运行时可能崩溃。
    const num = "123" as number; // ❌ 错误断言,但编译通过
    console.log(num.toFixed(2)); // 运行时报错
    
替代方案
  • 类型守卫(Type Guards)
    通过逻辑判断缩小类型范围。
    if (typeof value === "number") {
         
         
      value.toFixed(2); // 安全访问
    }
    </
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值