🔥Vue 面试硬核篇:Vue Router、Pinia、响应式源码原理……7 个章节深度解析
基础篇过关、进阶篇加分,这一篇决定你能不能拿 SP。
写在前面
这是 Vue 面试系列的第 3 篇(第 1 篇:核心基础 | 第 2 篇:Composition API 进阶)。
到这一篇,我们进入"硬核区":路由守卫的执行顺序你能一口气说完吗?Pinia 凭什么取代 Vuex?响应式源码(track/trigger/effect)能不能手写一遍?性能优化除了说"懒加载"还有没有别的货?
这一篇的特点:每节都附手写实现,源码题答得上手写代码,面试直接上一个档次。
适合谁看?
- ✅ 冲刺中大厂前端的同学(源码题重灾区)
- ✅ 用过 Router/Pinia 但被原理题问住的人
- ✅ 想在性能优化话题上说出 10 个点以上的开发者
阅读建议:全文约 1.3 万字。源码章节建议边读边在纸上画流程图,文末附速查表。
📑 目录
📌 附:面试知识点速查表
19. Vue Router
19.1 基本配置
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'), // 路由懒加载
meta: { title: '首页', requiresAuth: false }
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'),
props: true // 将 params 作为 props 传入组件(解耦,推荐)
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
children: [ // 嵌套路由
{ path: 'profile', component: () => import('@/views/Profile.vue') },
{ path: 'settings', component: () => import('@/views/Settings.vue') }
]
},
{
path: '/:pathMatch(.*)*', // 404 通配(Vue 4.x 语法)
name: 'NotFound',
component: () => import('@/views/404.vue')
}
];
const router = createRouter({
history: createWebHistory(), // history 模式;hash 模式用 createWebHashHistory()
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition; // 浏览器前进/后退恢复位置
return { top: 0 }; // 新页面回到顶部
}
});
19.2 路由使用
<script setup>
import { useRouter, useRoute } from 'vue-router';
const router = useRouter(); // 路由器实例:做导航
const route = useRoute(); // 当前路由:拿参数
console.log(route.params.id); // 动态参数
console.log(route.query.page); // 查询参数
// 编程式导航
router.push('/home');
router.push({ name: 'User', params: { id: 1 } }); // params 配 name 用
router.push({ path: '/user/1', query: { tab: 'a' } });
router.replace('/login'); // 不留历史记录
router.back(); // 后退(等效 go(-1))
// ⚠️ 易错点:params 传参要用 name,用 path 会被忽略!
// router.push({ path: '/user', params: { id: 1 } }) // ❌ id 丢失
</script>
<template>
<router-link :to="{ name: 'User', params: { id: 1 } }">用户</router-link>
<!-- 路由切换动画 + keep-alive 的标准组合 -->
<router-view v-slot="{ Component }">
<Transition name="fade" mode="out-in">
<KeepAlive :max="10">
<component :is="Component" />
</KeepAlive>
</Transition>
</router-view>
</template>
19.3 路由守卫
// 全局前置守卫:权限控制的主战场
router.beforeEach((to, from) => {
// 返回 false → 取消导航
// 返回路由地址 → 重定向
// 返回 undefined/true → 放行
if (to.meta.requiresAuth && !isAuthenticated()) {
return { name: 'Login', query: { redirect: to.fullPath } };
}
});
router.afterEach((to) => {
document.title = to.meta.title || 'App'; // 改标题
});
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
// 组件内守卫(<script setup> 写法)
onBeforeRouteUpdate(() => {
// 路由变了但组件被复用时(如 /user/1 → /user/2)
});
onBeforeRouteLeave(() => {
// 离开前:未保存表单弹确认框的经典场景
return confirm('有未保存的修改,确定离开?');
});
</script>
19.4 动态路由与权限
// 后端权限方案:登录后根据角色动态注册路由
router.beforeEach(async (to, from) => {
const token = getToken();
if (to.meta.requiresAuth && !token) return { name: 'Login' };
if (token && !router.hasRoute('Dashboard')) { // 还没注册过
const menus = await getUserMenus(); // 拉取该用户的菜单/路由
menus.forEach(m => router.addRoute(m)); // 动态注册
return to.fullPath; // ⚠️ 重新导航一次
}
});
💬 面试题精选
Q1:hash 模式和 history 模式有什么区别? ⭐必考
答:
| 特性 | hash 模式 | history 模式 |
|---|---|---|
| URL 样式 | /#/path(丑) | /path(干净) |
| 实现原理 | hashchange 事件(不请求服务器) | HTML5 pushState / popstate |
| 服务器配置 | 不需要 | 必须配置 404 fallback(刷新时把所有路径指到 index.html) |
| SEO | 差 | 好 |
| 适用 | 后台管理、不在意 URL | 面向用户的站点 |
💡 加分话术:history 模式下刷新页面浏览器会向服务器请求当前路径,Nginx 需要配
try_files $uri $uri/ /index.html,否则 404。hash 模式的#后面部分不会发给服务器,所以刷新永远不会 404。
Q2:导航守卫的执行顺序? ⭐必考
答(按触发顺序):
- 失活组件的
beforeRouteLeave - 全局
beforeEach - 复用组件的
beforeRouteUpdate - 路由独享的
beforeEnter - 解析异步路由组件
- 激活组件的
beforeRouteEnter(此时拿不到 this) - 全局
beforeResolve - 导航确认 → 全局
afterEach→ 触发 DOM 更新
💡 加分话术:
beforeRouteEnter是唯一拿不到组件实例的守卫(组件还没创建),要用next(vm => {...})(Vue 4.x 已改为 Promise:beforeRouteEnter(to, from)里返回 Promise)或直接在setup里用onBeforeMount处理。
Q3:如何实现前端路由权限控制?
答:两种主流方案——
- 静态路由 + meta 标记 + 全局守卫:路由全量注册,守卫里判断
to.meta.roles是否包含当前角色,不在则拦截 - 动态路由:登录后根据角色拉取菜单,用
router.addRoute()动态注册,配合 404 兜底
💡 加分话术:方案 2 更安全——没权限的路由根本不存在于前端路由表,直接访问 URL 也匹配不到;方案 1 只是"界面隐藏",路由本身还在,需要守卫兜底。实际项目常两者结合:公共路由静态注册,业务路由动态注册。
20. Vuex 状态管理
💡 Vuex 是"上一代"方案,新项目都用 Pinia 了。但面试依然会问——既问 Vuex 概念,更问"为什么被 Pinia 取代"。
20.1 核心概念
import { createStore } from 'vuex';
const store = createStore({
// state:单一状态树
state() {
return { count: 0, todos: [] };
},
// getters:派生状态(有缓存的 computed)
getters: {
doubleCount: (state) => state.count * 2,
// 返回函数,可以传参
getTodoById: (state) => (id) => state.todos.find(t => t.id === id)
},
// mutations:同步修改(devtools 才能追踪)
mutations: {
INCREMENT(state) { state.count++; },
SET_COUNT(state, count) { state.count = count; }
},
// actions:异步操作(提交 mutation)
actions: {
async fetchCount({ commit }) {
const res = await fetch('/api/count');
commit('SET_COUNT', await res.json());
}
},
modules: { /* 模块化 */ }
});
四个概念的职责一句话总结:state 存数据、getters 算数据、mutations 改数据(同步)、actions 做异步然后提交 mutations。
20.2 模块化与使用
// store/modules/user.js
export default {
namespaced: true, // ⚠️ 命名空间,防止命名冲突
state: () => ({ token: null, profile: null }),
getters: { isLoggedIn: (state) => !!state.token },
mutations: { SET_TOKEN(state, t) { state.token = t; } },
actions: {
async login({ commit }, credentials) {
const { token } = await api.login(credentials);
commit('SET_TOKEN', token);
}
}
};
// 使用(带命名空间要加前缀)
store.dispatch('user/login', credentials);
store.getters['user/isLoggedIn'];
<script setup>
import { useStore } from 'vuex';
import { computed } from 'vue';
const store = useStore();
const count = computed(() => store.state.count);
const doubleCount = computed(() => store.getters.doubleCount);
store.commit('INCREMENT'); // 提交 mutation
store.dispatch('fetchCount'); // 分发 action
</script>
💡 Vuex 与响应式原理的联系:state 是
reactive的,getters 是computed的——所以 state 一变,组件自动更新。
21. Pinia 状态管理
21.1 两种定义风格
// stores/counter.js
import { defineStore } from 'pinia';
// ① Option Store(像 Vuex)
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() { this.count++; }, // 不再需要 commit!
async fetchCount() {
const res = await fetch('/api/count');
this.count = await res.json(); // 直接改 state
}
}
});
// ② Setup Store(推荐,与 Composition API 完全一致)
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() { count.value++; }
async function fetchCount() {
const res = await fetch('/api/count');
count.value = await res.json();
}
return { count, doubleCount, increment, fetchCount };
});
21.2 使用与进阶 API
<script setup>
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
const store = useCounterStore();
// ⚠️ 解构 state/getters 必须用 storeToRefs 保持响应性
const { count, doubleCount } = storeToRefs(store);
// 方法可以直接解构(方法不依赖响应性)
const { increment } = store;
// 修改的四种方式
store.count++; // 直接改
store.$patch({ count: 5 }); // 批量改
store.$patch((s) => { s.count++; }); // 函数式批量改
store.$reset(); // 重置(仅 Option Store)
// 订阅:跨组件/跨模块联动、持久化
store.$subscribe((mutation, state) => {
localStorage.setItem('count', JSON.stringify(state.count));
});
</script>
💬 面试题精选
Q1:Pinia 和 Vuex 的区别?为什么新项目都选 Pinia? ⭐必考
答:
| 特性 | Vuex | Pinia |
|---|---|---|
| 修改数据 | mutations(同步)+ actions(异步),强制分离 | 只有 actions,直接改 state |
| TypeScript | 类型推断弱(this、模块嵌套难推断) | 原生支持,Setup Store 类型自动推断 |
| 模块化 | modules 嵌套 + namespaced,繁琐 | 每个 Store 独立扁平,天然隔离 |
| 体积 | 较大 | ~1KB |
| API | 有 mutation/action/getter/state 四概念 | state/getters/actions 三概念 |
| 使用体验 | mapState 等辅助函数 | storeToRefs 一行搞定 |
为什么 Pinia 更好:
- 去掉了 mutations——Vuex 强制"异步必须走 action → commit mutation",实际开发中 90% 的 mutation 只是赋值,纯样板代码;Pinia 允许 action 直接改 state,devTools 照样能追踪
- TS 体验质变:Setup Store 就是普通 Composition API,类型全自动推断
- 模块化零成本:不需要 namespaced 手动开关
💡 加分话术:Pinia 由 Vue 核心团队成员 Eduardo 开发,Vuex 5 的规划被 Pinia 吸收,官方已宣布 Pinia 是 Vuex 的继任者——新项目直接上 Pinia 即可,不用再纠结。
Q2:store 解构后为什么丢失响应性?怎么解决?
答:const { count } = store 相当于把 store 对象上的属性值拷贝到普通变量,count 变成一个普通数字。解决:state 和 getters 用 storeToRefs(store) 解构(内部转成了 ref),方法直接解构即可。
💡 加分话术:
storeToRefs本质上就是遍历 store 的响应式属性,用toRefs转成 ref——和 reactive 解构用 toRefs 是同一个问题。
22. Vue 3 新特性
💡 这一节不是罗列 API,而是回答"Vue 3 相比 Vue 2 到底新在哪",面试按"编译层 / 运行时层 / API 层"三层组织答案更显深度。
22.1 组件与模板层
<template>
<!-- ① Fragment:多根节点 -->
<header />
<main />
<footer />
<!-- ② Teleport:DOM 传送到 body(弹窗场景) -->
<Teleport to="body" :disabled="isInline">
<Modal />
</Teleport>
<!-- ③ Suspense:异步依赖统一占位 -->
<Suspense>
<template #default><AsyncPage /></template>
<template #fallback><Loading /></template>
</Suspense>
</template>
22.2 新增响应式 API(容易忽略的冷门考点)
import { ref, customRef, effectScope, markRaw, toRaw, unref } from 'vue';
// ① customRef:自定义 ref(防抖输入框的经典实现)
function useDebouncedRef(value, delay = 300) {
let timer;
return customRef((track, trigger) => ({
get() { track(); return value; },
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => { value = newValue; trigger(); }, delay);
}
}));
}
const keyword = useDebouncedRef('');
// ② effectScope:批量管理副作用,离开作用域统一停止
const scope = effectScope();
scope.run(() => {
watch(x, cb);
watchEffect(cb);
});
scope.stop(); // 全部停止(大型组件卸载优化、库开发常用)
// ③ markRaw / toRaw:与响应式系统划清界限
const lib = markRaw(new HugeLibrary()); // 永不代理,省性能
const plain = toRaw(reactiveObj); // 取原始对象
// ④ unref:写工具函数时的糖
unref(ref(0)); // 0(是 ref 就取 .value)
unref(0); // 0(不是 ref 原样返回)
💬 面试题精选
Q1:Vue 3 有哪些新特性?(组织答案的框架)
答,按层组织:
- 响应式层:
Proxy重写 +ref/reactive/computed/watch等独立 API +customRef/effectScope等高级 API - 组件层:Fragment 多根节点、Teleport、Suspense、
<script setup> - 编译层:静态提升、Patch Flags(动态标记)、Block Tree、事件缓存——运行时性能大幅提升的来源
- API 层:Composition API + Composables 逻辑复用
- 工程层:Tree-shaking(打包体积更小)、原生 TS 支持
💡 加分话术:编译优化是 Vue 3 性能提升的大头——模板编译时给动态节点打 Patch Flag(文本、class、style 等),运行时 diff 只比对这些标记,静态节点整块跳过,所以 Vue 3 的更新性能比 Vue 2 快 1.2~2 倍(官方数据)。
23. 响应式原理深入
⭐⭐ 这是本篇的重头戏,也是中大厂源码面的必考区。建议跟着代码手写一遍。
23.1 Vue 2:Object.defineProperty 的局限
// Vue 2 响应式核心逻辑简化版
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) dep.depend(); // 收集依赖
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 派发更新
}
});
}
四个致命局限(面试张口就来):
- ❌ 无法检测新增属性(没有提前 defineProperty)→
$set - ❌ 无法检测删除属性 →
$delete - ❌ 无法检测数组索引修改和
length变化 → 重写 7 个变异方法 - ❌ 初始化时递归遍历所有属性,大对象性能差
23.2 Vue 3:手写一个迷你响应式系统
// ========== 1. 依赖收集中心 ==========
const targetMap = new WeakMap(); // WeakMap: 目标对象 → (Map: key → Set<effect>)
let activeEffect = null; // 当前正在执行的副作用
// ========== 2. track:get 时收集 ==========
function track(target, key) {
if (!activeEffect) return; // 不在副作用中,不收集
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
dep.add(activeEffect);
}
// ========== 3. trigger:set 时派发 ==========
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
depsMap.get(key)?.forEach(effect => effect());
}
// ========== 4. reactive:Proxy 代理 ==========
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 收集
// ⭐ 惰性代理:只有访问到嵌套对象才递归代理
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 派发
}
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key); // 删除也通知!Vue 2 做不到
return result;
}
});
}
// ========== 5. effect:执行并记录自己 ==========
function effect(fn) {
activeEffect = fn;
fn(); // 执行时会触发 get → 被 track 收集
activeEffect = null;
}
// ========== 验证 ==========
const state = reactive({ count: 0 });
effect(() => console.log('count =', state.count));
// 输出: count = 0(执行时收集了依赖)
state.count++;
// 输出: count = 1(trigger 重新执行 effect)✅
state.nested = { a: 1 }; // 新增属性 ✅ 可检测
delete state.nested; // 删除属性 ✅ 可检测
23.3 依赖收集完整流程
组件渲染(render effect)
│ 访问响应式数据 state.count
▼
Proxy.get 拦截 ──track()──▶ activeEffect(当前渲染 effect)
存入 targetMap
│
数据变化:state.count++
▼
Proxy.set 拦截 ──trigger()──▶ 从 targetMap 取出 count 的 effect 集合
逐个执行 → 组件重新渲染
23.4 ref 的响应式原理(补充)
ref 内部就是一个 { value: x } 对象,通过 class 的 get value() / set value() 实现拦截,本质和 reactive 一样是 Proxy/defineProperty。所以 ref 能包基本类型——因为拦截发生在 .value 这一层,而不是对象属性层。
💬 面试题精选
Q1:Vue 3 响应式原理讲一下?(要求手写) ⭐⭐必考
答:核心三要素——Proxy 拦截、track 依赖收集、trigger 派发更新。然后按上面 23.2 的手写流程讲(面试官要的就是这个)。最后补两点加分项:
- 惰性代理:嵌套对象在 get 时才递归代理,初始化性能远好于 Vue 2
- WeakMap 设计:targetMap 用 WeakMap 存,target 对象被垃圾回收时映射自动清除,防止内存泄漏
Q2:什么是依赖收集?为什么要依赖收集?
答:依赖收集 = 记录"谁在用这个数据"。组件渲染时访问数据,把当前的渲染 effect 存到该数据名下的依赖集合中。数据变了,就知道该通知谁更新。
为什么需要:精确更新。没有这个机制,只能全量 diff 或全量重新渲染;有了它,哪个数据变了只重新执行依赖它的 effect,做到细粒度更新。
Q3:Vue 3 用 Proxy 为什么还是不能拦截基本类型?
答:Proxy 的 target 必须是对象。基本类型(string、number 等)不是对象,无法被代理——所以 Vue 3 仍然需要 ref 用 .value 包装一层对象来承载基本类型的响应式。这是 ref 存在的根本原因。
24. 性能优化
💡 面试问性能优化,最忌讳只说"路由懒加载、keep-alive"。按下面的"四层框架"答,体系感拉满。
24.1 加载性能(让用户更快看到页面)
// ① 路由懒加载:首屏只下载当前页面
{ path: '/about', component: () => import('./About.vue') }
// ② 组件懒加载:弹窗等低频组件
const Modal = defineAsyncComponent(() => import('./Modal.vue'));
// ③ 第三方库按需引入 + Tree-shaking
import { Button } from 'element-plus'; // 配 unplugin 自动按需
// ④ 图片优化:懒加载 + 懒加载 + 压缩 + CDN
<img src="a.jpg" />
// ⑤ 预加载/预取关键资源
<link rel="preload" href="critical.js" />
24.2 运行时性能(让页面更流畅)
<script setup>
import { shallowRef, markRaw } from 'vue';
// ① 大对象避开深层响应式
const bigList = shallowRef([/* 万级数据 */]);
const chart = markRaw(echarts.init(el)); // 第三方实例不需要响应式
// ② v-memo:列表项只有满足条件才重渲染(Vue 3.2+)
// <div v-memo="[item.id === selected]">...</div>
// ③ 大列表虚拟滚动
import { useVirtualList } from '@vueuse/core';
const { list, containerProps, wrapperProps } = useVirtualList(bigList, { itemHeight: 50 });
// ④ 防抖节流
import { useDebounceFn } from '@vueuse/core';
const onSearch = useDebounceFn(search, 300);
</script>
<template>
<!-- ⑤ v-once 标记纯静态内容,跳过更新 diff -->
<p v-once>版权信息 © 2026</p>
<!-- ⑥ 用 computed 代替模板里的复杂表达式,避免每次渲染重算 -->
</template>
24.3 更新性能(让 diff 更高效)
<template>
<!-- ① 列表 key 用唯一 id,不用 index(diff 能复用和移动节点) -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
<!-- ② v-for 不与 v-if 混用:过滤逻辑放 computed -->
<!-- ③ 细分组件:状态变化只重渲染最小子树,而不是整个大组件 -->
</template>
24.4 体验性能
- keep-alive 缓存 Tab 页,切回不白屏不重新请求
- SSR / SSG:首屏直出 HTML,解决白屏(见第 4 篇)
- 骨架屏 + 路由过渡动画,掩盖加载等待
💬 面试题精选
Q1:Vue 项目性能优化手段有哪些? ⭐必考
答(四层框架):
- 加载层:路由懒加载、组件懒加载、按需引入、图片懒加载压缩、HTTP 缓存/CDN、Gzip
- 运行时层:虚拟滚动、防抖节流、shallowRef/markRaw 避开深层响应式、v-memo/v-once、Web Worker 处理重计算
- 更新层:合理 key、组件拆细缩小重渲染范围、computed 缓存、避免模板复杂表达式
- 体验层:keep-alive、骨架屏、SSR/SSG
💡 加分话术:优化前先测量——用 Lighthouse 跑分、Vue DevTools 的 Performance 面板看组件渲染耗时、Chrome Performance 看主线程长任务,找到瓶颈再针对性优化,而不是盲目上手段。
Q2:v-memo 和 v-once 的区别?
答:
v-once:渲染一次后永久跳过更新,适合纯静态内容v-memo:接收依赖数组,依赖不变才跳过更新,适合"大部分时候不变、特定条件才变"的列表项(如选中态高亮的列表)
25. TypeScript 与 Vue
25.1 组件类型三件套
<script setup lang="ts">
// ① Props:泛型接口,类型 + 校验一次搞定
interface Props {
title: string;
count?: number;
user: { name: string; age: number };
}
const props = withDefaults(defineProps<Props>(), {
count: 0 // 有默认值的就不算必填
});
// ② Emits:调用签名(写错参数类型直接报错)
const emit = defineEmits<{
(e: 'update', value: string): void;
(e: 'delete', id: number): void;
}>();
// Vue 3.3+ 更简洁的元组写法:
const emit2 = defineEmits<{
update: [value: string];
delete: [id: number];
}>();
// ③ 模板 ref:标注元素类型
const inputRef = ref<HTMLInputElement | null>(null);
onMounted(() => inputRef.value?.focus());
</script>
25.2 组合式函数与 Store 的类型
// composables/useFetch.ts —— 泛型让返回值类型精确
import { ref, type Ref } from 'vue';
interface UseFetchReturn<T> {
data: Ref<T | null>;
error: Ref<Error | null>;
loading: Ref<boolean>;
}
export function useFetch<T>(url: string): UseFetchReturn<T> {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(true);
// ...
return { data, error, loading };
}
// 使用:泛型自动推断
const { data } = useFetch<User[]>('/api/users');
// data 的类型是 Ref<User[] | null> ✅
// stores/user.ts —— Pinia Setup Store 类型全自动
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
interface User { id: number; name: string; }
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null);
const isLoggedIn = computed(() => !!user.value);
function setUser(u: User) { user.value = u; }
return { user, isLoggedIn, setUser };
// 返回什么类型就是什么类型,零额外标注 ✅
});
💬 面试题精选
Q1:如何在 Vue 3 中用好 TypeScript?
答五个要点:
<script setup lang="ts">开启 TSdefineProps<接口>()替代运行时对象写法(类型即文档)defineEmits<{...}>()约束事件参数- Composables 用泛型
<T>让返回值可推断 - Pinia 用 Setup Store,状态类型自动推导
💡 加分话术:
withDefaults+ 泛型 Props 最大的好处是"类型即校验"——TS 编译时报错提前到写代码阶段,且不需要写validator函数那套运行时代码了。
Q2:ref 的类型为什么写作 Ref<number> 而不是 number?
答:因为 ref(0) 返回的是一个包装对象 { value: 0 },它的类型是 Ref<number>——这个对象才是响应式载体。解构时 storeToRefs / toRefs 干的事就是把属性转成 Ref<T>,保住这层包装。
📋 面试知识点速查表
| 问题 | 一句话答案 |
|---|---|
| hash vs history | #不请求服务器 vs 需配 fallback;hashchange vs pushState |
| params 传参 | 必须用 name,用 path 会丢 |
| 导航守卫顺序 | 离开→beforeEach→update→beforeEnter→beforeRouteEnter→beforeResolve→afterEach |
| 权限方案 | meta+守卫(隐藏)或 addRoute 动态注册(真正隔离),后者更安全 |
| Vuex 四概念 | state存/getters算/mutations同步改/actions异步改 |
| Pinia vs Vuex | 无 mutations、TS 原生、扁平 store、~1KB |
| store 解构丢响应 | storeToRefs(state/getters),方法直接解构 |
| Vue3 新特性分层 | 响应式层/组件层/编译层(Patch Flags)/API层/工程层 |
| Proxy 为什么快 | 惰性代理 + 细粒度依赖收集 + 编译期 Patch Flags |
| 手写响应式三要素 | Proxy 拦截 + track 收集 + trigger 派发(WeakMap 防泄漏) |
| ref 为什么能包基本类型 | value 层拦截,Proxy 需要对象 |
| 性能优化四层 | 加载/运行时/更新/体验,先测量再优化 |
| v-memo vs v-once | 依赖数组控制 vs 永久静态 |
| TS 三件套 | defineProps<接口>/defineEmits<签名>/withDefaults |
📌 最后
- 硬核篇值得点赞 ❤️ + 收藏 ⭐ 反复看,尤其响应式手写部分,面试前自己默写一遍
- 最终篇(SSR / 测试 / 性能优化落地 / 30 道高频面试题精讲)即将发布,关注追更, series 完结时会放出全系列目录索引
- 有任何问题或勘误,评论区见 🙏