一、前言
面试篇(下):组件通信全家桶、路由与 Pinia、以及手写题环节。手写题给可直接默写的最短正确版,都源自系列实现过的代码。
Q1:Vue3 组件通信方式全说出(025-030/034 篇)
简答(背这张表,按"关系"记):
| 关系 | 方式 | 出处 |
|---|---|---|
| 父 → 子 | props | 025 |
| 子 → 父 | emit | 026 |
| 父 ↔ 子 | 组件上的 v-model(modelValue) | 027 |
| 祖先 → 后代(跨层) | provide / inject | 028 |
| 父 → 子(内容分发) | slot / 作用域插槽 | 029 |
| 父 → 子(属性透传) | $attrs | 033 |
| 父拿子 | ref + defineExpose | 033 |
| 任意关系 | Pinia 全局状态 | 053-058 |
加分展开:选型原则------层级浅用 props/emit;跨层共享用 provide/inject 或全局状态(053 篇的判断标准:"离开组件树还有意义吗");slot 是"结构由父决定、数据由子提供"的反转场景。
Q2:路由守卫的执行顺序?(049 篇)
简答:完整导航流程:
text
导航触发
→ 全局 beforeEach
→ 路由独享 beforeEnter
→ 组件内 beforeRouteEnter / beforeRouteUpdate / beforeRouteLeave
→ 全局 resolve 的 beforeEach 完成后:beforeResolve
→ 导航确认 → afterEach
→ DOM 更新 → beforeRouteEnter 的 next 回调(拿到实例)
加分展开:登录拦截放全局 beforeEach(meta.requiresAuth);页面标题设置放 afterEach(导航确认后再改 document.title 才对);每个钩子"return false / return 路由地址"即可中断/重定向,不需要调 next(Vue Router 4 推荐 return 风格)。
Q3:params 和 query 的区别?(047 篇)
简答 :params 是路径的一部分(/user/1,需要路由配置 :id),刷新还在(路径就是它);query 是地址栏问号参数(?tab=2),不需要路由配置,刷新也还在。真正的坑:params 通过对象编程式传参且不在 path 里声明的写法在 Vue Router 4 已被移除。
加分展开:传"不能出现在 URL 的数据"(编辑表单的大对象)→ 用路由 state/历史 state 或 Pinia 暂存;props 解耦(:props="true")让组件收参数不依赖 $route------组件可测性更好。
Q4:keep-alive 原理与缓存策略?(030/073 篇)
简答 :keep-alive 是抽象组件,内部用 Map 缓存组件实例的 vnode,命中缓存直接复用(不重新走 created/mounted),配合 include/exclude/max 控制。被缓存组件切换时触发 activated/deactivated 而非销毁。
加分展开 :LRU 策略(max 满了淘汰最久未用的);典型坑:include 匹配组件 name;列表页缓存 + onActivated 脏标记刷新是标准组合拳。
Q5:Pinia 为什么取代 Vuex?(053 篇)
简答 :四个字------砍掉中间商:没有 mutation,action 直接改 state;TS 完整推导;天然多 store 模块化(不需要 modules 嵌套);DevTools 支持完整。Pinia 是 Vue3 官方推荐,Vuex 处于维护态。
加分展开:
- $patch 合并多处修改为一次更新(055 篇)
- storeToRefs 解构保响应式(055 篇)
- 持久化插件解决刷新丢失(057 篇)
- Setup Store 与组合式函数心智统一(058 篇 TS 全推导)
Q6:手写防抖 debounce(038/076 篇)
ts
function debounce(fn: Function, delay = 500) {
let timer: ReturnType<typeof setTimeout> | null = null
return function (...args: any[]) {
if (timer) clearTimeout(timer) // 重新来过:取消上一次
timer = setTimeout(() => {
fn(...args) // 停手 delay 后才执行
timer = null
}, delay)
}
}
加分展开:两个必问延伸------①立即执行版(首次触发先执行,用于按钮)加一个 leading 参数;②为什么 setTimeout 里的 this/args 要闭包保存(保证执行上下文和参数正确)。一句话场景收尾:搜索框输入。
Q7:手写节流 throttle(038/076 篇)
ts
function throttle(fn: Function, interval = 200) {
let last = 0
return function (...args: any[]) {
const now = Date.now()
if (now - last >= interval) { // 距上次执行够久才放行
last = now
fn(...args)
}
}
}
加分展开:时间戳版(本文)首尾完整但最后一次可能丢;定时器版最后一次必执行但首击延迟------两者结合是完整版。场景收尾:滚动监听、resize。
Q8:手写一个最简组合式函数 useToggle(036 篇)
ts
import { ref } from 'vue'
function useToggle(initial = false) {
const state = ref(initial)
const toggle = (next?: boolean) => {
state.value = next ?? !state.value // 传参指定值,不传取反
}
return [state, toggle] as const // ★ as const 保持元组类型
}
加分展开 :as const 是 TS 细节(解构后类型不被拉宽成 boolean | Function);this 题眼:组合式函数的通用规范------输入响应式参数、返回响应式结果,VueUse 全部如此(079 篇)。
Q9:手写按钮权限指令 v-permission(034/070 篇)
ts
import type { Directive } from 'vue'
const vPermission: Directive<HTMLElement, string | string[]> = {
mounted(el, binding) {
const roles = useUserStore().info?.roles ?? []
const need = Array.isArray(binding.value) ? binding.value : [binding.value]
if (!need.some((r) => roles.includes(r))) {
el.parentNode?.removeChild(el) // 无权限直接移除
}
}
}
加分展开:为什么移除而不是 display:none(隐藏可被 F12 破解);运行时角色会变则改用 v-if 方法式;永远补一句"后端必须再校验"体现安全意识。
Q10:讲讲虚拟列表的实现思路(074 篇)
简答:只渲染可视区的几十条数据。三步:①容器内放一个"总高度占位元素"撑出真实滚动条;②监听 scroll,用 scrollTop ÷ 行高算出 startIndex;③可视区内容切片渲染并 translateY 到对应位置,上下加缓冲防露白。
加分延伸 :不定高方案 = 先估高渲染、IntersectionObserver/实测后纠正(DynamicScroller);大数据表格直接用 el-table-v2 虚拟化;性能本质是把 DOM 数量从 O(n) 降到 O(可视区)。
三、答题心法
text
1. 先结论后展开:一句话定调 → 细节 → 场景/项目印证
2. 手写题先写"最短正确版",被追问再升级(立即执行版、定时器版、并发控制)
3. 所有答案都能落到自己的博客/项目------"我踩过这个坑"是最强加分项
四、今日小结
- 组件通信背关系表 (8 种 + 选型原则);守卫顺序先全局后组件,return 风格
- 手写四连:防抖(闭包+清除)、节流(时间戳)、useToggle(as const)、v-permission(移除而非隐藏)
- 虚拟列表口诀:占位撑滚动、scrollTop 算起点、切片渲染加缓冲
- 面试本质:把 88 篇踩过的坑讲给面试官听
下篇预告
最终章:88 篇完结复盘------从环境搭建到项目上线的完整技术地图,项目结构终稿、学习方法沉淀、进阶路线(Nuxt / 源码 / 全栈)一个不落。下一篇 088。