公司动态
Vue 3面试核心考点全解析:从响应式原理到性能优化
1. 先说点实际的Vue 面试到底在考什么又到金三银四的跳槽季了后台收到不少读者留言说自己在准备 Vue 方向的前端面经时被各种“八股文”折磨得焦头烂额。尤其是 2026 年这个节点Vue 3 已经是绝对主流面试官问问题的方式也早就不一样了——很少再有人直接问你“Vue 的生命周期是什么”而是给你一个实际场景让你现场分析为什么这个组件不更新、为什么这段代码会产生死循环、computed 和 watch 到底该选哪个。我一向主张一个观点面试题不是用来背的是用来理解框架设计思路的。所以这篇 Vue 面经我不想列一个简单的“面试题标准答案”的清单而是想把面试官最喜欢追问的几个大方向拆开讲清楚背后的设计逻辑和考察意图。你把这些东西吃透了哪怕面试官换个花样来问你也能从容应对。这篇面经适合三种人第一种是准备跳槽的初中级前端需要系统梳理 Vue 的知识体系第二种是在公司用 Vue 写业务写了两三年、但一直停留在“会用”层面的同学想补一补原理方面的短板第三种是准备带团队或者做技术方案选型的人需要通过面试反推自己对框架的理解是否成体系。我尽量按照面试官真实提问的顺序来组织内容从响应式原理到组件通信到路由与状态管理再到性能优化和手写题。每部分都会结合我实际面试别人和被面试时遇到过的问题来展开。文章会有点长你可以先收藏再慢慢看也可以直接跳到你最薄弱的部分。2. 响应式系统从 Vue 2 问到 Vue 3 的那些坑2.1 面试官为什么死磕响应式原理响应式系统是 Vue 的根可以说 Vue 的所有高级特性——computed、watch、模板更新、组件复用——全都建立在这套机制之上。面试官问你响应式原理表面上是在考源码实际上是想判断你对框架的理解深度。一个只知道用 data 和 methods 写页面的开发者和一个能解释“为什么 data 必须是函数”“为什么 Vue 3 用 Proxy 替换了 Object.defineProperty”“为什么直接按下标修改数组不会触发更新”的开发者在面试官心里的价值是完全不同的。我面试别人的时候有个习惯问完响应式原理之后会紧接着抛一个业务问题“假如你有个列表数据某个按钮点击后修改了其中某一项的某个字段页面却迟迟不刷新你最可能的排查步骤是什么”这就是典型的从原理到排查的考察方式。如果你能立刻想到是响应式丢失、是深层对象没有提前声明、或者是在赋值时用了整体替换而不是修改属性那说明你是真懂而不是在背。2.2 Object.defineProperty 的三大局限Vue 3 为何弃用Vue 2 的响应式核心是 Object.defineProperty它通过 getter/setter 拦截对象的属性访问和修改再配合发布订阅模式实现视图更新。但这个东西有很明显的三个短板每一个都对应一个真实的坑。第一个是对象属性层面的短板。Object.defineProperty 只能拦截已经存在的属性对于后添加的新属性完全没有感知。这就是为什么 Vue 2 需要提供 Vue.set / this.$set 这样的 API因为如果你直接 this.obj.newProp 1对象本身虽然多了这个属性但 getter/setter 没有挂上去自然也就没有响应式。很多新手第一次遇到“为什么我赋了值页面却不更新”就是因为走了这条老路。第二个是数组层面的短板。JavaScript 数组本质上也是对象但直接通过索引修改元素比如 arr[0] 1Object.defineProperty 是拦不住的。Vue 2 只能靠重写 push、pop、shift、unshift、splice、sort、reverse 这七个方法来变相处理。每次修改数组长度、每次用索引改值都得小心绕路。这也是 Vue 2 面试题里最高频的一个考点。第三个是性能层面的短板。Object.defineProperty 需要提前递归地遍历对象的所有属性给每个属性都定义 getter/setter。对象层级越深初始化消耗越大。而且这个操作是在初始化时就完成的不管这个属性最后有没有被用到都得白白付出一遍遍历的开销。在大数据量的场景下Vue 2 的初始化性能会受到明显影响。Vue 3 直接换成了 Proxy从根本上解决了这三个问题。Proxy 是代理整个对象而不是每个属性所以新增属性和数组索引修改都能被拦截也不需要递归遍历整个对象来绑定 getter/setter而是在读取到某个属性时才进行依赖收集用到一个子对象才去深度代理这个子对象。这个“懒处理”的策略让初始化性能好了不少也让响应式行为更符合直觉。不过注意Proxy 的兼容性比 Object.defineProperty 差IE11 不支持。所以如果你们公司的产品还有老 IE 的兼容要求Vue 3 可能就不太适用。这也是面试官喜欢追问的延伸问题——为什么会有人还在用 Vue 2不是老古董而是有真实的技术约束。2.3 computed 的缓存机制一道送分题但很多人拿不到分computed 可以说是 Vue 面试里的频率之王也是我见过被误解最多的 API。很多人只知道“computed 有缓存watch 没有缓存”但被问到“这个缓存是怎么实现的”“为什么依赖没变computed 就不会重新计算”时就卡住了。我帮你把这层窗户纸捅破。computed 在 Vue 3 内部对应的是一个 computedRefImpl 实例它维护了两个关键标志dirty 和 value。第一次访问 computed 的值时dirty 为 true所以它会执行你传入的 getter 函数把计算结果赋值给 value再把 dirty 置为 false。之后只要依赖的响应式数据没有变化dirty 就一直是 false后续访问会直接 return 缓存的 valuegetter 不再执行。对应地computed 内部会创建一个 effect这个 effect 的 scheduler 里做的事情就是把 dirty 重新置为 true。当依赖数据变化时不立即重新计算而是先标记“脏了”等下一次访问 computed 值时再重新计算。这个“懒计算脏检查”的设计就是 computed 缓存机制的核心。这个机制带来的实际收益很明显。比如你有一个 filterData 计算属性依赖一个很大的列表和一个搜索关键词模板里同时有多个地方引用 filterData。没有缓存的话每次渲染都重新 filter 一遍浪费大量计算有缓存的话只要列表和关键词不变filter 结果就一直复用。这对列表筛选、表单联动、大对象派生数据这类场景性能提升是实打实的。面试官的常规追问是“computed 和 watch 都能监听数据变化什么时候用 watch”我的建议是能用 computed 解决的就不要用 watch。computed 是声明式的、基于依赖自动缓存的天然适合“由某个数据经过计算得到另一个数据”的场景watch 更适合“数据变化后需要执行异步操作、请求接口、操作 DOM”这种有副作用的场景。不要把 watch 当万能药很多由 watch 引起的性能问题和死循环其实换成 computed 就能轻松解决。2.4 nextTick 到底在等什么几乎每个候选人都知道 nextTick 能用但聊到“Vue 的异步更新队列”就露馅了。Vue 的数据更新不是同步的。当你修改一个响应式数据时Vue 不会立刻更新 DOM而是把这次更新任务推进一个队列里等到当前事件循环的微任务阶段或者某些降级场景下的宏任务再统一执行。这样做的好处是如果你在一段代码里连续修改了三次同一个数据不会触发三次 DOM 更新而是只触发一次。把多次同步修改合并成一次渲染这个机制就是所谓的“批处理”。nextTick 返回的 Promise 之所以能拿到最新的 DOM是因为它注册的 this.tick 任务会被塞进更新队列之后。更新队列执行完DOM 渲染完成再执行 nextTick 里的回调自然就能拿到更新后的 DOM 了。我遇到过一个真实案例一个同事用 Vue 2 写表格组件点击展开按钮后需要立刻拿到展开区域的高度来设置动画结果在点击事件里直接拿高度永远是 0。原因就是 DOM 还没更新dom 里的内容是旧的。我当时给他的建议就是在 this.$nextTick 里再取高度问题立刻解决。这个例子我在面试中讲给候选人听过大家普遍反应比单纯背“nextTick 是异步的”要直观得多。面试到这块有时候还会延伸问“Vue 3 的 nextTick 和 Vue 2 有什么区别”。其实本质上没变都是把回调函数推入一个 Promise 微任务队列只不过 Vue 3 的更新调度器用了一个叫做 job queue 的队列配合更细粒度的 effect 调度。你只要把“事件循环 微任务批处理”这个概念理清楚怎么变都能应对。3. 组件通信与生命周期父子组件渲染顺序背后的设计意图3.1 八种组件通信方式你真的全掌握了吗组件通信是 Vue 业务开发里避不开的基础功也是面试里最容易“一问就会一写就废”的板块。我这里把 Vue 2 和 Vue 3 通用的通信方式做一个全景梳理你可以对照检查自己还有哪块是模糊的。第一种是 props 和 $emit这是父子组件最直接、最常用的通信方式。父组件通过 props 向子组件传数据子组件通过 $emit 事件通知父组件。注意在 Vue 3 里如果是使用 setup 语法糖子组件通过 defineEmits 声明事件父组件通过 custom-event 监听。第二种是 v-model本质上是 props 和事件绑定的语法糖。Vue 3 里组件上的 v-model 等价于传了 modelValue 这个 prop并监听 update:modelValue 事件。一个组件可以支持多个 v-model比如 v-model:title 和 v-model:content这在封装复杂表单组件时尤其好用。第三种是 ref 和 $refs父组件可以直接访问子组件的实例或 DOM 元素。这种方式适合在一次性的命令式操作里使用比如父组件调用子组件的某个方法聚焦输入框。但注意不要滥用否则容易让数据流变得不可预测。第四种是 $parent / $children通过组件实例直接访问父子链路。Vue 3 里 $children 被移除了$parent 还在但官方并不推荐在业务代码里依赖这种隐式关系因为组件一旦嵌套层级变深这种写法会让数据来源极难追踪。第五种是 provide / inject用于跨层级共享数据。某个祖先组件 provide 一段数据下面无论嵌套多深的子孙组件都可以通过 inject 获取。这套机制适合做全局依赖注入、主题配置、国际化等场景。Vue 3 的 provide 还可以传 ref 或 reactive 对象实现跨组件响应式共享这是官方推荐的替代部分 Vuex 场景的方案。第六种是事件总线 EventBus在 Vue 2 里常被用来做跨组件通信本质上就是 new Vue() 然后到处 $on/$emit。Vue 3 里官方推荐用 mitt 或者直接使用 provide/inject 来替代。EventBus 最大的问题是事件满天飞之后代码根本没法调试事件从哪里 emit、哪里监听都没法静态检查。第七种是 Vuex / Pinia 这类全局状态管理库适用于多组件共享数据且数据需要被多方修改的场景比如用户登录信息、购物车数据、全局配置等。Vue 2 的项目用 VuexVue 3 的项目我强烈建议直接用 Pinia。第八种是插槽 Slot。严格说插槽不是数据通信但它实现了模板内容的分发父组件能把任意结构传进子组件子组件也能通过作用域插槽向父组件反向传递内部数据。这种模式在写通用组件库的时候非常重要。3.2 生命周期顺序题你会不会答出面试官想要的层次生命周期是前端面经里最基础的内容但基础不意味着低级。面试官真正想听的不只是背出 created、mounted、unmounted 这几个名字而是父子组件的生命周期执行顺序以及这个顺序对业务代码的影响。一个父组件包含一个子组件首次渲染时执行顺序是这样的父组件 setup / beforeCreate → 父组件 created → 父组件 beforeMount → 子组件 setup / beforeCreate → 子组件 created → 子组件 beforeMount → 子组件 mounted → 父组件 mounted。有点反直觉的是子组件的 mounted 居然在父组件 mounted 之前。原因是 Vue 的挂载过程是递归的父组件必须先渲染出子组件的虚拟节点再去真正挂载子组件子组件挂载完成后父组件才能继续完成自己的挂载。理解了这个顺序你就能解释为什么在子组件的 mounted 里拿不到父组件某些稍后才初始化的数据也能解释为什么父组件有一些初始化逻辑依赖子组件时要放在 nextTick 或者 mounted 之后。更新阶段也要注意父组件数据变化触发更新时顺序默认是父组件 beforeUpdate → 子组件 beforeUpdate → 子组件 updated → 父组件 updated。子组件先于父组件更新完成这样才能保证父组件更新时子组件已经是新状态了。还有一套不能忽略的 API 是 keep-alive 相关钩子activated 和 deactivated。当组件被 keep-alive 缓存时组件不会走销毁流程而是从“失活”变成“激活”。activated 在每次进入时触发deactivated 在每次离开时触发。很多人在处理 Tab 页签列表缓存时把“切换 tab 恢复数据”的逻辑写错地方本质就是没搞懂组件是走 mounted 还是走 activated。被 keep-alive 包裹的组件首屏进页面会执行 mounted 和 activated之后切换回来只执行 activated不会再走 mounted。我通常在面试里给候选人一个实际场景题“现在有一个订单列表页和一个订单详情页用户从列表进入详情再返回列表你希望列表保持滚动位置怎么做”这道题有不少加分点能用 keep-alive 加 include 控制缓存能在 deactivated 里记录滚动位置能在 activated 里恢复滚动位置。如果你能顺带提到 keep-alive 的 include/exclude 匹配规则以及 max 属性控制缓存数量面试官基本就能确认你写过真实项目而不是只看了教程。3.3 v-model 的原理拆解让你彻底告别组件的“数据同步焦虑”v-model 在组件通信里的地位被严重低估了。很多人只会用 input 上的 v-model一旦遇到自定义组件要双向绑定就不知道从何下手。我直接用代码拆开v-model 的本质就是 props emit 的语法糖。以内置 input 为例等价于input :valuesearchText inputsearchText $event.target.value /组件上使用 v-model 时等价于CustomInput :modelValuesearchText update:modelValuesearchText $event /所以在子组件里你要做的事情就是声明一个 modelValue prop并在需要更新时调用 emit(update:modelValue, newValue)。这里有个细节Vue 3 默认的 prop 名是 modelValue 而不是 value很多从 Vue 2 迁移过来的同学在这里踩过坑。进阶玩法是多个 v-model。比如一个筛选组件同时需要限制 minPrice 和 maxPrice 两个值你可以这样写在 PriceRange 组件内部对应声明 min 和 max 两个 props并分别 emit update:min 和 update:max。这种多 v-model 的写法让自定义组件的 API 设计变得非常干净父组件不需要写一堆监听事件来处理两个值的变化。面试官有时会继续深挖“v-model 能修饰符吗自定义修饰符怎么实现”Vue 3 支持在 v-model 上使用自定义修饰符比如 v-model.trimprops 里会额外接收一个 modelModifiers 参数你可以根据这个对象来判断是否需要做特殊处理。这类问题很少出现在基础面经里如果你能答出来会给面试官留下“研究过官方文档和源码”的印象。如果你准备时间有限我建议至少把默认 v-model 的原理和多个 v-model 的用法练熟。4. 路由与状态管理从 URL 变化到页面渲染的完整链路4.1 Vue Router 的两种模式别只答出 hash 和历史模式的区别Vue 面经里路由相关的题目几乎必考。最常见的问题就是“Vue Router 有哪几种模式它们有什么区别”这个问题的标准答案大家都知道hash 模式在 URL 里有 # history 模式没有。# 的问题在于不好看而且在某些场景下分享链接会带一个奇怪的符号。history 模式是利用 history.pushState 和 popstate 实现的URL 看起来更美观但需要服务器端配置 history fallback否则刷新页面会 404。但如果面试官紧接着问“history 模式为什么刷新会 404背后的原理是什么”很多人可能就讲不清楚了。这个原理其实也不复杂history 模式是前端路由URL 的变化全靠 JS 的 pushState 来改变并不会真的向服务器发起请求。所以你在某个路由比如 /order/detail 上按 F5 刷新时浏览器会向服务器请求 /order/detail 这个地址而服务器端没有做任何配置的话它不知道这个路径应该返回 index.html就会找不到资源于是返回 404。解决办法也很直接在 Nginx 里配置 try_files让所有路径都回退到 index.htmllocation / { try_files $uri $uri/ /index.html; }但要注意这种配置也有一个隐患就是前端的 404 路由级别会失效。因为无论你访问什么路径服务器都会返回 index.html真正确定 404 的是前端路由判断。所以你需要在前端路由表里配置一个 catch-all 路由来兜底未知路径。还有一个高频追问是“路由传参方式”。URL 参数对应的是 this.$route.queryrequest 参数动态路由对应 this.$route.params路径参数。query 传参会把参数放在 URL 问号后面params 对应的则是 /user/:id 这种路径。一个常见的问题是使用对象写法并配合 name 跳转时params 不会丢失但如果直接写 pathparams 会被忽略。这是 Vue Router 的一个经典坑面经里也经常被点名你需要特别注意。4.2 路由守卫的完整执行顺序和应用场景路由守卫有两种理解方式一种是官方文档里的全局守卫、路由独享守卫、组件内守卫三层体系另一种是理解 Vue Router 在做导航解析时的完整执行链路。面试官问到守卫往往是想了解你在权限控制、登录拦截、页面跳转埋点这些实际场景里是怎么做的。全局守卫里最常用的是 beforeEach比如登录拦截的逻辑router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ name: login, query: { redirect: to.fullPath } }) } else { next() } })在 Vue Router 4 里守卫函数可以直接返回一个路由地址或者 false 来中断当前导航next 函数依然可用但官方更推荐返回值写法。我建议你用返回值写法逻辑更清晰也更容易测试。路由独享守卫是写在路由配置里的 beforeEnter只在该路由被进入时触发。组件内守卫有 beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave 三个它们分别对应进入前、组件被复用时比如同一个组件在不同参数间切换、离开前。完整的导航解析流程大概是这样触发导航 → 失活组件里调用 beforeRouteLeave → 全局的 beforeEach → 如果是路由参数变化导致复用组件执行组件的 beforeRouteUpdate → 路由配置里的 beforeEnter → 激活组件的 beforeRouteEnter → 全局的 beforeResolve → 导航被确认 → DOM 更新 → 组件内 beforeRouteEnter 里的 next 回调执行。这个流程面试官如果追问细节你能完整说出来就已经超过大半候选人了。实际项目中beforeRouteEnter 的 next 回调可以拿到 vm 实例来做一些初始化操作但注意 beforeRouteEnter 钩子里 this 还不存在因为组件实例还没创建。这是很多人在面试手写题时容易忽略的点。4.3 Vuex 与 Pinia 的对比2026 年的新项目还用 Vuex 吗Vuex 和 Pinia 的对比在 2026 年基本没有悬念了新项目直接选 Pinia。我在推荐新项目状态管理方案时已经不会再考虑 Vuex除非团队成员的 Vue 2 项目里有历史包袱。Pinia 相比 Vuex 最大的优势是去掉了 mutations直接把 action 当成普通函数来写。这意味着你在 Pinia 里只需要关心 state 和 action不需要记住“同步改 state 走 mutation、异步走 action 再 commit mutation”这套繁琐的流程。代码量更少心智负担更低对 TypeScript 的支持也更好。从面试角度看你需要掌握 Pinia 的三种写法。选项式 storeexport const useUserStore defineStore(user, { state: () ({ name: , age: 0 }), getters: { nameLength: (state) state.name.length }, actions: { async fetchUser() { const res await api.getUser() this.name res.name } } })组合式 storeexport const useUserStore defineStore(user, () { const name ref() const age ref(0) const nameLength computed(() name.value.length) async function fetchUser() { const res await api.getUser() name.value res.name } return { name, age, nameLength, fetchUser } })组合式 store 其实就是把 setup 风格的代码搬进了状态管理里对于已经在用 setup 语法写组件的团队来说上手极其顺滑。我建议你在面试时提一下“Pinia 底层是基于 reactive 实现的所以 state 本身就是响应式的”这句话能体现出你对 Pinia 原理有认知而不是只会调 API。Vuex 方面虽然新项目不推荐了但面试仍然可能考因为很多存量项目还在用。你需要掌握 state、getters、mutations、actions、modules 这五个核心概念还要能说清楚为什么 mutations 必须是同步的——因为 Vuex 需要依靠 mutations 的同步执行来配合 devtools 的状态回放和插件的时间旅行调试。如果你说不上来就会显得是背题。5. diff 算法与性能优化面试官递进式追问的正确答案5.1 虚拟 DOM 的价值不只是“快”这么简单聊 diff 算法之前先把虚拟 DOM 这东西的意义说清楚。很多面经把虚拟 DOM 解释成“比真实 DOM 快”这个说法其实不太严谨。在极小的页面里直接操作真实 DOM 反而可能更快因为虚拟 DOM 还要经历对比计算的开销。虚拟 DOM 的核心价值是让开发者以声明式的方式描述界面然后框架负责把“描述”高效地同步到真实 DOM。这个抽象的价值体现在三方面。第一跨平台能力。因为虚拟 DOM 只是普通的 JavaScript 对象它不依赖浏览器环境所以能跑在服务端渲染SSR、小程序、原生应用等多种渲染环境里。第二可追踪可测试。框架能在虚拟 DOM 层做变更追踪、记录快照方便 devtools 审查。第三开发体验。开发者写的是状态描述不需要手动操作 DOM避免了一堆 appendChild / removeChild 散落在业务代码里。diff 算法要解决的问题是当数据变化后如何用最小的代价把新的虚拟 DOM 树同步到真实 DOM 上。Vue 2 的双端 diff 和 Vue 3 的快速 diff 我都推荐研究一下但理解重点不应该放在逐行源码上而应该放在“key 的作用”和“为什么尽量不要用 index 当 key”这两个高频考点上。5.2 key 为什么不能用 index一个几乎没有争议的面试结论面试官问“key 有什么用”标准回答是key 是虚拟 DOM 的标识diff 算法通过 key 来判断哪些节点是复用的哪些节点需要新增或删除从而减少不必要的 DOM 操作。但为什么不能用 index我给你一个最直观的例子。假设你有一个列表数组渲染了三个 li{{ item.name }}现在你在数组头部插入一条新数据。原本 key 为 0、1、2 的三个节点现在变成 key 为 1、2、3。diff 算法看 key会认为原来的 key1 节点还在于是复用它来渲染新数组里 index1 的那条数据。但实际上你期望的是新插入的数据渲染在第一位原来的三条数据保持原位更新。这种错位在纯文本场景下可能没露馅但如果你在 li 内部还有子组件、有输入框、有图片状态就会出大问题——所有“被复用”的组件都会接收到错误的数据输入框的值也不会自动清空。更合理的方案是使用业务数据里唯一的 id 作为 key。如果列表里的数据确实没有稳定 id也至少要保证每条数据有一个业务上恒定的唯一标识。这个点在实际项目踩坑里太常见了尤其在写动态表单、拖拽排序、可编辑列表这类功能时。我自己就见过一个同事在写可编辑表格时因为用了 index 作为 key导致某一行内容明明是 A编辑后却显示成了 B 的内容——排查了半天才发现是 key 的问题。5.3 性能优化清单从运行到构建能聊的比你想的多前端性能优化的提问范围很广但 Vue 项目里你至少要把下面这九个点准备好因为它们跟框架强相关随便挑一个都能扩展出实际案例。路由懒加载。Vue Router 支持动态导入把路由对应的组件拆成独立 chunk访问到该路由时才下载对应代码。这能显著减少首屏加载的 JS 体积。写法是 component: () import(/views/Home.vue)。组件异步加载。如果你的页面里有一个很重但不一定展示的组件比如一个大型弹窗里的复杂表单或视频播放器可以考虑用 defineAsyncComponent。Vue 3 里这样写const HeavyComponent defineAsyncComponent(() import(./HeavyComponent.vue))配合 Suspense 可以做加载状态比在 mounted 里手动判断再挂载要优雅得多。keep-alive 的使用。前面讲生命周期时提过这里不再展开。优化时要能说清楚 keep-alive 在长列表、Tab 页签、表单填写的场景里怎么帮你减少重复渲染和保留状态。v-once 和 v-memo。v-once 用于标记“只需要渲染一次”的节点之后即使数据变化也不会更新。v-memo 是 Vue 3.2 引入的用于条件性地跳过某些节点的更新。这两个指令在超大列表、复杂静态结构场景下能减少不必要的渲染但一般业务代码里用得不多面试时提到就行表示你了解 Vue 3 的新特性。响应式数据的合理设计。避免把一个很大的、不需要响应式的对象放进 data可以用 shallowRef / shallowReactive 或 markRaw 来跳过深层代理。例如一个上万条数据的表格但只有翻页时整体替换数据就不需要深度响应式改用一个 shallowRef 就够了。我之前帮一个同事优化一个拖拽大量节点时卡顿的 Canvas 应用就是把节点数据改成 shallowReactive帧率一下子从十几帧提到了满帧。防抖和节流不是 Vue 专有但面试里经常结合事件处理来问。比如搜索框输入时实时请求接口需要防抖监听滚动位置做懒加载需要节流。你可以顺带说一句Vue 3 里可以在 composable 里封装 useDebounce这样比在模板里写一堆 setTimeout 更容易维护。列表虚拟滚动。对于一个可能渲染几千条 DOM 的列表虚拟滚动几乎是必答题。原理是只渲染视口可见区域的那几条数据通过监听滚动计算出哪几项需要渲染再通过 padding 或 transform 来撑起整个容器高度。手写一个基础版本不算难面试时能讲清楚思路就很有说服力。如果项目里用到 vue-virtual-scroller 这类库也可以提一下。使用 defineComponent 和组合式函数抽取逻辑。这里更多是代码组织层面的优化不是性能优化但它能提高可维护性面试官问“你怎么组织复杂业务逻辑”时可以用上。一个几百行代码的轮播组件如果把核心逻辑拆成 useCarousel组件本身的代码量会急剧下降性能排查也会更容易。构建层面的优化。Vite 项目中常见的优化手段包括preload 关键资源、代码分割、Tree Shaking、压缩、CDN 加速。你可以提一下怎么用 rollup-plugin-visualizer 看构建产物然后针对性地拆分 vendor chunk。这一层在高级别面试里会被提到能答出来就是加分项。另外在 Vue 项目里有时需要播放 HLS 格式的 M3U8 视频流。这个场景和 Vue 结合时最常用的方案是封装一个基于 hls.js 的播放组件在 mounted 中拉取 m3u8 地址再调用 hls 实例播放。要注意的是Android 端部分浏览器对 hls.js 兼容性不稳定需要做降级让原生 video 标签直接播放。这个知识点虽然不完全属于面试高频题但很多安防监控、直播项目都会遇到项目中提过会显得你经验丰富。至于大文件上传如果项目里要处理前端往往用 Web Worker 做分片然后在主线程使用 SparkMD5 计算文件校验和再分片上传。Vue 组件里可以通过 new Worker(new URL(worker.js, import.meta.url)) 这种方式创建 Worker避免 worker 文件被打包成独立 chunk 时找不到路径。这也是一个很能体现工程能力的细节。6. 手撕代码与工程化套题哪些 Vue 题一定要会写6.1 手写响应式Vue 3 核心模型的精简版面试的最后阶段很多公司会安排手写题。Vue 方向的手写题里写一个简化版的响应式系统出现频率相当高。你不用把 Vue 源码抄出来只需要用 Proxy 实现依赖收集和触发的核心机制就行。一个常见的简化版本大概是这样的let activeEffect null const targetMap new WeakMap()function track(target, key) { if (!activeEffect) return let depsMap targetMap.get(target) if (!depsMap) { depsMap new Map() targetMap.set(target, depsMap) } let dep depsMap.get(key) if (!dep) { dep new Set() depsMap.set(key, dep) } dep.add(activeEffect) }function trigger(target, key) { const depsMap targetMap.get(target) if (!depsMap) return const dep depsMap.get(key) if (dep) { dep.forEach((effect) effect()) } }function reactive(target) { return new Proxy(target, { get(target, key, receiver) { const value Reflect.get(target, key, receiver) track(target, key) return value }, set(target, key, value, receiver) { const result Reflect.set(target, key, value, receiver) trigger(target, key) return result } }) }function effect(fn) { activeEffect fn fn() activeEffect null }effect(() { console.log(user.profile.name , user.profile.name) })这个版本的逻辑是effect 执行时会把当前函数设置为 activeEffect函数内部读取响应式数据时触发 getget 里把 activeEffect 收集到对应的 key 下当 set 修改数据时触发 trigger执行所有收集到的 effect。面试官有时候会追问“为什么用 Reflect 而不是直接 target[key]”。主要原因是 this 指向问题。当对象有 getter 且 getter 内部访问 this 时通过 Proxy 的 get 拦截去做 target[key] 会把 this 指向原始对象导致无法代理 prototype 链上的属性和方法而 Reflect.get(target, key, receiver) 可以把 this 绑定到代理对象 receiver从而保持一致的拦截效果。如果你能在这个代码示例之外再补充一句“嵌套对象的递归代理是懒处理的访问时才代理子对象”面试官基本就满意了。能写出来这个版本说明你是真的理解 Vue 3 的响应式闭环而不是只会调 API。6.2 手写 EventBus 和 computed考察你对“发布订阅”本质的理解EventBus 手写版本在 Vue 2 时代几乎是必考题现在虽然不常用了但它考察的是一个非常基础的能力发布订阅模式。一个最简版本class EventBus { constructor() { this.events new Map() } on(event, callback) { if (!this.events.has(event)) { this.events.set(event, []) } this.events.get(event).push(callback) } emit(event, ...args) { if (!this.events.has(event)) return this.events.get(event).forEach((cb) cb(...args)) } off(event, callback) { if (!this.events.has(event)) return if (!callback) { this.events.delete(event) return } const callbacks this.events.get(event).filter((cb) cb ! callback) this.events.set(event, callbacks) } }实际使用时这类工具还会考虑 once 一次性监听以及 off 时防止重复绑定。Vue 3 生态里 mitt 就是这么个微型库源码也就 80 行。你可以对照看一下自己写的是否覆盖了这些边界。手写 computed 可以这样思考它需要一个 effect并且要管理 dirty 状态。之前讲缓存原理时已经提过这里我列出核心逻辑伪代码function computed(getter) { let dirty true let value const effectFn effect(getter, { lazy: true, // 默认不执行 getter scheduler() { dirty true // 通知依赖该 computed 的地方重新运行 trigger(obj, value) } }) const obj { get value() { track(obj, value) if (dirty) { value effectFn() dirty false } return value } } return obj }由于不是完整源码我用伪代码帮助理解即可面试时能讲清楚依赖收集与脏检查的配合已经足够。6.3 工程化套题Vite 构建、devtools、环境配置这类“不怎么像面试题”的问题除了手写题Vue 面试的工程化部分也经常被拉出来问。这里有一个容易被忽略的点如果你简历里写了熟悉 Vite面试官大概率会追问“Vite 为什么快”。你要能回答出核心区别Vite 开发环境使用原生 ES Module冷启动时不打包整个项目只启动一个开发服务器浏览器按需请求模块而 Webpack 在启动时就要递归构建整个依赖图。所以项目越大Vite 的优势越明显。构建走的是 Rollup产物优化能力强但有一些 CommonJS 依赖兼容问题需要配置 optimizeDeps。Vue Devtools 这个工具在面试里其实也经常被聊到。它是排查 Vue 应用问题的神器。面试官可能问“你怎么调试响应式数据的问题”你要是能说出我会用 Vue Devtools 的组件树去查看组件的 props/state用 timeline 观察事件和依赖追踪在数据不更新时去 inspect 响应式数据状态这比空谈“我会 console.log”要靠谱得多。我自己排查生命周期不更新问题时第一步就是打开 devtools 看组件是否被缓存、当前是否处于 activated 状态。还有一类问题表面上看是环境配错其实考察的是 Vue 的工程基础比如“用 Vue 官网中文 3.0 提供的脚手架创建项目依赖安装总是报错怎么办”。这类问题我的建议是先看报错信息里的依赖版本冲突优先检查 node 版本是否满足要求然后调整 registry 源最后考虑删除 node_modules 和 lock 文件重新安装。如果你能说出“用 pnpm 的 lock 机制在同等条件下比 npm 更稳定”这种话面试官会高看你一眼。另外高版本的 Node.js 与某些老依赖包不兼容问题往往出在 node-sass 这类原生模块上使用 sass 替代能省去很多麻烦。这一节最后提醒一下工程化问题没有标准答案面试官更多是看你的排查思路和组织方式。你要做到的是被问到任何一个工具链问题时都能完整给出“报错现象 → 排查思路 → 解决步骤”的链路而不是只给出一个结论。7. 一些面试之外的话写到这里核心内容基本讲完了。最后分享一点我自己准备 Vue 面试的经验不要试图背完所有知识点而是把框架的响应式系统、组件通信、生命周期、路由与状态管理、diff 与性能优化这五条主线理解透再配合手写题加深记忆面试基本不会有死角。我遇到过很多候选人明明项目经验丰富但一到面试就紧张原因就是脑子里装的全是单独的知识碎片没有形成体系。你可以在面试前用一张白纸尝试默写 v-model 展开后的代码、computed 的缓存流程、父子组件的生命周期顺序、Vue Router 导航守卫的完整链路。如果能默写出来说明你有体系默不出来就把对应章节重新过一遍直到能流畅默写为止。最后提醒一句现在的面试越来越不满足于你会用而是要求你知道为什么。把 Vue 当成一个“响应式系统 组件系统 渲染器”的组合来理解你会发现很多面试题根本不需要背推出来就行。祝准备跳槽的朋友们都拿到满意的 offer。