在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 对对象的属性进行劫持,将其转化为 getter 和 setter。
-
依赖收集(Getter):
当组件渲染时访问数据属性,触发getter,将当前的 Watcher(依赖)收集到依赖列表中(Dep)。 -
触发更新(Setter):
当数据被修改时,触发setter,通知所有依赖的 Watcher 更新视图。
局限性:
- 无法检测对象属性的新增或删除:
必须通过Vue.set或Vue.delete显式操作才能触发响应式更新。 - 对数组的监听受限:
直接通过索引修改数组(如arr[0] = 1)或修改数组长度(如arr.length = 0)无法触发更新。
Vue 2 通过重写数组的push、pop、splice等变异方法(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 实现更全面的拦截能力。
-
Proxy 的拦截操作:
Proxy 可以拦截对象的get、set、deleteProperty等操作,无需逐个劫持属性。 -
深层响应式:
通过递归代理嵌套对象,实现深层次的响应式跟踪。
优势:
- 支持动态新增/删除属性:
直接操作obj.newKey = value或delete 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时触发组件的created和mounted钩子;条件变为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 的第三方库初始化。
- 触发时机: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 官方方案):
- 集中式状态管理,通过
state、mutations、actions管理数据流。 - 示例:
// 组件中获取状态 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)**
使用场景
-
明确知道值的实际类型
当开发者比编译器更清楚值的类型时,强制指定类型。// 从外部数据源获取的值 const data = JSON.parse('{ "name": "Alice" }') as { name: string }; -
处理联合类型的窄化
在无法通过类型守卫(typeof/instanceof)缩小类型范围时,手动断言。const element = document.getElementById("input") as HTMLInputElement; -
兼容旧代码或第三方库
处理类型定义不完整的第三方库返回值。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); // 安全访问 } </
Vue3、Pinia与TypeScript面试题汇总

最低0.47元/天 解锁文章
1710

被折叠的 条评论
为什么被折叠?



