一、基础概念与原理
1. Vue 的 MVVM 模式是什么?
Vue 的设计思想遵循 MVVM(Model–View–ViewModel)架构模式,其核心目标是通过数据驱动的方式,实现视图与数据状态的自动同步,从而降低 UI 层的复杂度。
- Model(模型层):负责业务数据和业务逻辑的管理,通常对应组件中的
data、state或来自后端的接口数据。 - View(视图层):负责数据的展示,对应模板(template)中的 DOM 结构,本身不直接参与业务逻辑处理。
- ViewModel(视图模型层):作为 Model 与 View 之间的桥梁,负责:
- 将 Model 中的数据映射到 View
- 监听数据变化并触发视图更新
- 处理视图层的用户交互并反向更新数据
在 Vue 中,ViewModel 主要由 响应式系统 + 模板编译 + 虚拟 DOM 更新机制 共同实现。
工作原理:
- Vue 使用响应式系统(
Object.defineProperty/Proxy)对数据进行代理,追踪依赖。 - 当数据变化时,触发依赖收集的 Watcher 更新 DOM(视图自动更新)。
- 视图操作(如 input 输入)通过事件绑定更新数据,实现 数据 → 视图、视图 → 数据 的双向绑定。
面试关键点:
- 能说出 Vue 怎么跟踪依赖
- 能说明视图自动更新的流程
2. Vue2 与 Vue3 的核心区别
| 维度 | Vue2 | Vue3 |
|---|---|---|
| 响应式实现 | Object.defineProperty,无法监听新增/删除属性,性能稍低 | Proxy,全量监听,支持新增/删除属性,性能更好 |
| API 风格 | Options API(data、methods、computed、watch) | Composition API(setup + reactive/ref/computed/watch) |
| 性能优化 | Diff 算法优化有限 | 支持 Fragment、Tree-shaking、Lazy loading、Teleport、Suspense |
| 体积 & 构建 | 较大,Tree-shaking 不完全 | 更轻量,支持模块化按需打包 |
(1)响应式系统的差异
- Vue 2
- 基于
Object.defineProperty对对象属性进行劫持 - 需要在初始化阶段递归遍历所有属性
- 无法监听新增 / 删除属性
- 数组需要通过重写方法(push、splice 等)实现响应式
- 基于
- Vue 3
- 基于
Proxy对整个对象进行代理 - 支持对象属性的新增、删除和数组索引变化
- 按需代理,避免深度递归,性能更优
- 基于
(2)Composition API(组合式 API)
Vue 3 引入 Composition API,用于解决 Vue 2 中 Options API 存在的以下问题:
- 逻辑分散在 data / methods / computed / watch 中
- 复杂组件中相关逻辑难以维护和复用
- 类型推导对 TypeScript 不友好
Composition API 通过 setup、ref、reactive、computed 等方式:
- 让相关逻辑按功能聚合
- 提高代码复用能力
- 显著改善 TypeScript 支持
但 Composition API 并不是要完全取代 Options API,而是为复杂业务场景提供更灵活、更可维护的组织方式。
(3)性能与编译优化
Vue 3 在编译阶段和运行时都进行了大量优化:
- 静态提升(Static Hoisting):将静态节点提升到渲染函数外部,避免重复创建
- Patch Flag:在编译阶段标记动态节点,减少虚拟 DOM diff 的范围
- Tree-shaking 支持:核心模块按需引入,减小打包体积
这些优化使 Vue 3 在渲染性能和包体积方面整体优于 Vue 2。
面试关键点:
- Proxy vs defineProperty 的本质区别
- setup 的作用与优势
- Tree-shaking 如何减少未使用代码
3. Vue 生命周期钩子函数及作用
Vue 生命周期用于描述组件从创建到销毁的完整过程,不同阶段会暴露对应的钩子函数,方便开发者在合适的时机执行副作用逻辑。
(1)创建阶段
beforeCreate:数据和事件尚未初始化,无法访问data、propscreated:数据已初始化完成,可以访问data、props、methods;DOM 尚未挂载,无法操作真实 DOM- 常见用途:初始化数据、发起接口请求(不依赖 DOM 的场景)
(2)挂载阶段
beforeMount:模板已编译完成,虚拟 DOM 已生成,但尚未渲染到页面mounted:真实 DOM 已渲染完成,可以安全地进行 DOM 操作- 常见用途:DOM 操作、依赖 DOM 的第三方库初始化、部分异步请求
(3)更新阶段
beforeUpdate:数据已变化,DOM 尚未更新updated:DOM 已根据最新数据完成更新- 常见用途:需要在 DOM 更新后执行的逻辑(需谨慎,避免死循环)
(4)销毁阶段
- Vue 2:
beforeDestroy/destroyed - Vue 3:改名为
beforeUnmount/unmounted,整体语义更加统一;组合式 API 对应onBeforeUnmount/onUnmounted - 常见用途:清理定时器、取消事件监听、销毁第三方插件实例,防止内存泄漏
面试关键点:
- 能说出 mounted 发请求的原因
- 能说明 unmounted 时如何清理副作用
- 能解释创建 vs 挂载 vs 更新 vs 销毁的区别
这一节的关键是:理解响应式系统如何驱动视图、生命周期如何管理副作用、以及 Vue 3 为什么要做这些设计升级。
二、核心机制与优化
4. 响应式原理实现
考察点:ref/reactive + 依赖收集;判断点:能讲 track/trigger、深层对象/数组的处理
Vue 的响应式系统是其核心能力之一,本质上通过 拦截数据访问与修改行为,在数据变化时自动触发视图更新。
Vue 2 的响应式实现
Vue 2 基于 Object.defineProperty 对对象属性进行递归劫持:
- 在初始化阶段递归遍历 data 中的每一个属性
- 通过 getter 进行依赖收集(Dep)
- 通过 setter 在数据变化时通知 Watcher 更新视图
- 采用典型的发布–订阅模式管理依赖关系
该方案的局限性主要体现在:
- 无法监听对象属性的新增和删除
- 无法直接监听数组索引变化
- 初始化时需要深度递归,存在性能开销
Vue 3 的响应式实现
Vue 3 改用 Proxy 对整个对象进行代理:
- 在
get/set/deleteProperty等操作中统一拦截 - 支持动态属性添加、删除及数组索引修改
- 按需触发依赖收集,避免全量递归:访问属性时 track 收集依赖,修改属性时 trigger 派发更新
ref 与 reactive 的区别
| 特性 | reactive | ref |
|---|---|---|
| 数据类型 | 对象、数组 | 基本类型或对象/数组都行 |
| 响应方式 | Proxy 拦截对象属性 | 内部通过 .value 包裹 |
| 使用场景 | 多属性对象、嵌套对象 | 单值、简单状态、组合式 API |
深层对象和数组的处理
- 对象嵌套:Proxy 会递归代理,每层访问时才收集依赖
- 数组:拦截数组方法(push、pop、splice 等),保证修改可触发视图更新
一句话总结:Vue 2 的响应式是“属性级劫持”,Vue 3 是“对象级代理”,从根本上解决了监听能力受限和初始化性能问题。
面试话术:Vue3 的响应式原理就是通过 Proxy 对对象或数组进行代理拦截,通过 track 收集依赖,trigger 触发更新;ref 用于基本类型或单值,reactive 用于对象或数组;实现数据变化自动驱动视图更新,支持深层属性和组合式 API。
5. 虚拟 DOM 与 Diff 算法的作用
考察点:减少真实 DOM 操作;判断点:能说 key 的作用和优化点
虚拟 DOM 的作用
虚拟 DOM 本质上是对真实 DOM 的一种 JavaScript 对象抽象,其核心作用在于:
- 将 DOM 操作转化为 JS 层的计算
- 减少直接操作真实 DOM 带来的性能开销
- 提供跨平台渲染能力(如 Web、SSR、原生)
需要注意的是:虚拟 DOM 并不是一定比真实 DOM 快,而是通过减少无效 DOM 操作提升整体性能稳定性。
Diff 算法的作用
Diff 算法用于对比新旧虚拟 DOM 树,找出最小更新路径,只更新变化的节点,避免整棵树重渲染。其核心策略包括:
- 同层级比较:不跨层级对比,降低算法复杂度
- Key 标识节点身份:通过唯一 key 精准复用节点,保持节点状态稳定(如输入框值、组件状态)
- 最小化 DOM 操作:只更新发生变化的部分
在 Vue 3 中,结合 Patch Flag 与编译阶段优化(静态节点提升 hoistStatic、Block Tree),进一步缩小了 Diff 的对比范围,提高了渲染效率。
性能优化实践
- 静态节点提升(hoistStatic)
- 异步组件(defineAsyncComponent + Suspense)
- keep-alive 缓存组件
- v-once、v-memo 等静态标记
一句话总结:Diff 算法的优化重点不在“完全避免比较”,而在于“尽可能少比较、尽可能少更新”。
面试话术:Vue 使用虚拟 DOM 将真实 DOM 抽象为 JS 对象,通过 Diff 算法对比新旧节点,只更新变化部分,从而提高性能。v-for 使用 key 可以保证列表节点复用和状态稳定,同时提升渲染效率。
6. Computed 和 Watch 的区别
考察点:派生状态 vs 副作用;判断点:缓存、懒计算、深度监听、immediate
computed 和 watch 都用于处理响应式数据的变化,但关注点和使用场景不同。
Computed(计算属性)
- 基于依赖追踪机制,具备缓存特性:依赖不变时,重复访问直接返回缓存值
- 懒计算(lazy):只有访问时才会执行
- 必须返回一个值
- 更适合描述由已有状态派生出的新状态
典型场景:列表过滤、排序;多个状态组合生成展示数据;根据用户输入动态计算显示值。
示例:
const fullName = computed(() => `${firstName.value} ${lastName.value}`)Watch(侦听器)
- 监听指定数据源的变化,每次变化都会触发回调,不具备缓存
- 支持异步逻辑和副作用操作
- 支持
deep深度监听、immediate立即执行 - 更适合处理数据变化引发的行为
典型场景:监听路由参数变化请求接口;表单输入联动校验;数据变化后触发防抖、节流或写入 localStorage。
示例:
watch(
() => route.params.id,
(id) => fetchDetail(id),
{ immediate: true },
)对比总结
| 特性 | Computed | Watch |
|---|---|---|
| 类型 | 派生状态 | 副作用 |
| 缓存 | 有 | 无 |
| 执行方式 | 懒计算 | 数据变化立即执行(可异步) |
| 场景 | 模板显示、派生计算 | 请求接口、响应变化逻辑 |
核心区别:computed 用于“状态 → 状态”,watch 用于“状态变化 → 行为”;能用 computed 的场景,通常不推荐使用 watch。
面试话术:Computed 用于计算状态,依赖不变时自动缓存;watch 用于监听数据变化执行副作用,支持深度监听和立即执行。两者结合 reactive/ref 可实现高效状态管理。
三、组件与通信
7. 组件通信方式
考察点:父子通信、跨层通信、全局通信;判断点:能说清每种方式的适用场景与原理
父子通信
- 父传子:通过
props向子组件传值。单向绑定,子组件不能直接修改父组件状态;需要修改时通过事件或回调通知父组件。 - 子传父:通过
$emit触发事件。事件驱动,保持组件解耦;典型场景如表单组件向父组件传递用户输入。
跨层通信:provide / inject(依赖注入)
- 父组件
provide数据,后代组件inject数据 - 无需逐层传递 props,适合深层组件共享状态
- 注意:仅适合静态或初始化状态,需要响应式时应传入
reactive/ref数据
全局状态管理:Pinia / Vuex
- 管理全局共享状态,跨模块、跨页面通信方便
- 典型场景:登录用户信息、购物车、全局配置等
兄弟组件 / 事件总线
- Vue 2 常用事件总线(Event Bus)或通过父组件中转;Vue 3 推荐 Pinia / provide-inject,中小型项目可用
mitt创建事件总线 - 注意:事件总线容易造成事件管理混乱,不推荐大型项目使用
面试话术:组件通信根据层级和用途选择方案,父子通信用 props/emit,跨层用 provide/inject,跨模块用 Pinia,全局状态统一管理可提高可维护性。
8. 动态组件与异步组件
考察点:提高组件复用性与性能;判断点:能说 <component :is>、defineAsyncComponent、懒加载
动态组件
- 使用
<component :is="currentComponent" />在同一位置动态渲染不同组件 - 使用场景:tab 页、步骤组件、条件渲染不同表单
异步组件
- 通过
defineAsyncComponent或路由懒加载() => import('./Component.vue')实现按需加载 - 优点:按需加载,减少首屏包体积
- 使用场景:大型页面、弹窗组件、路由页面懒加载
面试话术:动态组件通过 <component :is> 可以在同一位置渲染不同组件,异步组件可按需加载,减少初始包体积,提高性能。
9. 插槽(Slots)的应用场景
考察点:组件内容分发与复用;判断点:默认插槽、具名插槽、作用域插槽
- 默认插槽:组件内部使用
<slot />接收父组件内容。场景:内容可替换,简单分发。 - 具名插槽:组件内部使用
<slot name="header" />指定插入位置。场景:复杂布局,多区域可替换。 - 作用域插槽:子组件通过
v-bind把数据传给父组件,由父组件利用数据渲染。场景:列表渲染、表格列渲染、组件可配置模板。
插槽提高组件复用性和灵活性:默认插槽适合简单替换,具名插槽适合多区域布局,作用域插槽适合父组件利用子组件数据渲染。
面试话术:插槽用于内容分发,默认插槽简单替换,具名插槽适合多区域布局,作用域插槽可让父组件渲染子组件数据,实现高复用组件。
四、路由与状态管理
10. Vue Router 的导航守卫
Vue Router 的导航守卫可以分为三类:全局守卫、路由独享守卫和组件内守卫。
全局守卫
beforeEach:每次路由跳转开始时最先执行,常用于登录状态校验、权限判断和路由白名单控制;不满足条件时可中断或重定向导航。beforeResolve:所有守卫执行完成、组件渲染之前触发,适合依赖最终路由状态的逻辑,例如精确埋点或确认跳转结果。afterEach:路由跳转完成后执行,不能中断导航,一般用于页面访问统计、修改document.title等纯副作用操作。
路由独享守卫
- 在路由配置中使用
beforeEnter定义,只对某一个路由生效,适合某些页面有特殊权限或前置条件判断的场景,避免将逻辑写进全局守卫。
组件内守卫
beforeRouteEnter:进入组件前调用,此时组件实例还未创建(无法访问this),可以做数据校验或权限确认。beforeRouteUpdate:当前路由改变但组件被复用时调用。beforeRouteLeave:离开组件前调用,常用于表单未保存的离开提示。
面试话术:Vue Router 的守卫用于控制路由跳转流程,全局守卫适合通用的权限和登录控制,路由独享守卫适合单页面特殊逻辑,组件内守卫更关注页面级生命周期和用户操作控制。
11. Vuex / Pinia 的核心概念
Vuex 的核心概念:
- State:存储全局状态
- Mutations:同步修改状态(通过
commit触发) - Actions:异步操作后提交 Mutations(通过
dispatch触发) - Getters:计算派生状态,类似计算属性
Pinia 去除了 Mutation 的概念,直接在 Action 中修改 State,使模型更加简单直观;同时基于组合式 API,更符合 Vue3 的开发习惯。
为什么需要集中式状态管理?
避免多组件之间通过层层 props 或事件传递共享数据,统一管理全局状态,使数据来源清晰、修改路径可追踪,有利于大型中后台项目的维护和扩展。
面试话术:在复杂应用中,状态往往会被多个组件依赖,集中式状态管理可以统一管理这些全局状态,减少组件耦合,提升可维护性。Pinia 相比 Vuex 设计更轻量,更符合 Vue3 的开发习惯。
五、高频实战问题
12. v-if 与 v-show 的区别
| 特性 | v-if | v-show |
|---|---|---|
| 实现方式 | 条件为 true 时渲染元素,false 时销毁 DOM | DOM 一直存在,通过 CSS 的 display 控制显示/隐藏 |
| 渲染开销 | 条件切换时会销毁/重建 DOM,开销大 | 初始渲染时渲染 DOM,切换显示隐藏开销小 |
| 适用场景 | 条件不频繁切换,如页面入口、动态表单字段 | 条件频繁切换,如标签页切换、弹窗显示隐藏 |
| 生命周期 | 切换会触发组件的 created/mounted/unmounted | 生命周期只在初始渲染时触发,不会重复执行 |
补充说明:
- v-if 在条件很少变化的场景性能更优,因为避免了初始不必要的渲染。
- v-show 初始渲染消耗高,但切换速度快,适合频繁显示隐藏的场景。
13. v-for 为什么需要 Key?
- Key 用于 唯一标识节点身份,让 Vue 能正确追踪节点对应关系。
- 优化 Diff 算法 的效率,避免在列表更新时出现渲染错误或 DOM 重排。
- 使用示例:
<li v-for="item in list" :key="item.id">{{ item.name }}</li> - 不使用 key 可能会导致:删除或新增列表项时,Vue 复用错误的 DOM 节点,出现数据错位。
14. Vue 性能优化手段
缓存静态内容
v-once:渲染一次后不再响应数据变化v-memo:缓存渲染结果,减少重复计算
路由懒加载 / 组件异步加载
- 减少首屏加载体积,提高渲染速度;组件按需加载,避免一次性加载全部
避免深层响应式对象
- 深层对象响应式会增加依赖追踪开销
- 对不需要响应式的数据可以用
Object.freeze()冻结
其他优化方法
- 合理拆分组件,减少不必要的重渲染范围
- 使用 computed 替代频繁调用的 methods
- 渲染大列表时使用虚拟列表,减少 DOM 节点数量
15. 模板渲染流程
考察点:Vue 模板到真实 DOM 的渲染过程、setup 中数据如何绑定到模板;判断点:能描述 template → render function → VNode → DOM 的完整流程
流程说明:Vue 的模板首先被编译为 render 函数,再由 render 函数生成虚拟 DOM(VNode),最终 Vue 对比新旧 VNode,将差异更新到真实 DOM。
setup 数据绑定:组合式 API 中,setup 返回的数据、方法、计算属性会被绑定到模板形成响应式依赖。数据变化时,通过依赖追踪机制触发对应 VNode 更新,实现视图自动刷新。
面试话术:模板渲染流程可以总结为 template → 编译 → render function → VNode → DOM。在 setup 中返回的数据会建立响应式依赖,数据变化会驱动模板对应的 DOM 更新。
六、原理类高频问题(自测)
以下 10 道原理题建议先自己作答,再看括号里的提示:
Object.defineProperty和Proxy在实现上有何本质区别?Vue3 改用 Proxy 解决了哪些痛点?(提示:数组监听、动态属性、性能开销)- 依赖收集和派发更新的具体流程是怎样的?请描述 Dep 和 Watcher 的协作关系。(提示:发布-订阅模式、getter/setter 触发时机)
- Vue 的模板编译过程经历了哪几个阶段?如何将模板字符串转换为渲染函数?(提示:解析器生成 AST → 优化器标记静态节点 → 代码生成器生成 render 函数)
- 虚拟 DOM 的 Diff 算法中,为什么要优先进行“同层比较”而不是跨层递归?Key 值在 Diff 过程中起到了什么作用?(提示:时间复杂度优化、节点复用策略)
- Vue 的异步更新队列是如何工作的?为什么修改数据后立即访问 DOM 可能获取不到最新值?(提示:nextTick 实现原理、事件循环与微任务)
- Vue3 的静态提升和 Block Tree 机制如何优化渲染性能?(提示:跳过静态节点比对、动态节点标记 Patch Flags)
- 计算属性的缓存特性是如何实现的?与普通方法调用有何本质区别?(提示:脏检查机制、依赖追踪)
- Vue 组件实例化过程中,data 选项为什么要用函数返回对象,而不是直接写对象?(提示:避免多个实例共享同一数据引用)
- Vue 的事件系统是如何实现 v-on 的?事件修饰符(如
.native)底层做了哪些处理?(提示:原生事件与自定义事件的分发机制) - Vue3 的 Composition API 如何解决逻辑复用问题?对比 Mixins 和 Hooks 的优缺点。(提示:命名冲突、代码组织、类型推导)