Vue面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 885月28日 03:20

什么是 MVVM 模式?它是为了解决什么问题?

MVVM 是 Model-View-ViewModel 的缩写,是一种将 UI 表现层与业务逻辑层分离的架构模式。三层的职责:Model:数据与业务逻辑,包括数据模型、API 请求、数据校验等View:UI 展示层,负责渲染界面和接收用户交互ViewModel:连接 Model 与 View 的桥梁,通过数据绑定让两者自动同步MVVM 要解决的核心问题是 View 与 Model 之间的直接耦合。在传统 MVC 中,View 可能直接读取 Model 数据,Model 变化后手动通知 View 更新,随着业务增长,Controller 膨胀成 Massive View Controller,View 和 Model 互相引用导致维护困难。MVVM 通过 ViewModel 把两者彻底解耦——View 只依赖 ViewModel,Model 完全不知道 View 的存在,通信通过数据绑定自动完成。Vue 是典型的 MVVM 实践:template 是 View,data/computed 是 Model,Vue 实例充当 ViewModel。通过响应式系统(Vue 2 的 Object.defineProperty / Vue 3 的 Proxy)拦截数据变化,配合模板编译和虚拟 DOM diff,实现 View 与 Model 的自动同步,开发者不再需要手动操作 DOM。双向绑定的实现原理双向绑定是 MVVM 的核心机制,Vue 的实现分为三个关键部分:数据劫持(Observer):递归遍历 data 对象,用 Object.defineProperty(Vue 2)或 Proxy(Vue 3)拦截属性的 get/set,每个属性对应一个 Dep(依赖收集器)模板编译(Compiler):解析模板中的指令(v-model、插值语法、v-bind 等),为每个绑定创建一个 Watcher依赖收集与派发更新(Watcher + Dep):get 时收集依赖(Watcher 订阅 Dep),set 时通知 Dep 派发更新,Watcher 触发视图重新渲染以 v-model 为例:编译时为 input 元素添加 input 事件监听,用户输入时将值赋给数据属性(View 到 Model);同时为该属性创建 Watcher,属性变化时更新 input 的 value(Model 到 View),由此形成双向数据流。MVVM 与 MVC、MVP 的区别| 对比项 | MVC | MVP | MVVM ||--------|-----|-----|------|| 通信方式 | View 和 Model 可直接通信 | View 和 Model 通过 Presenter 通信 | View 和 Model 通过 ViewModel + 数据绑定通信 || 中间层职责 | Controller 处理路由和输入 | Presenter 包含业务逻辑 | ViewModel 暴露视图状态 || View 与 Model 耦合 | 可能直接引用 | 完全解耦 | 完全解耦 || 手动同步 | 需要手动更新 View | Presenter 手动调用 View 接口 | 数据绑定自动同步 || 可测试性 | 较低 | 较高 | 最高(ViewModel 无 UI 依赖) |MVC 适合服务端渲染场景(如 Rails),MVP 适合 Android 早期开发,MVVM 适合数据驱动的 UI 框架(Vue、WPF)。选择哪种模式取决于项目复杂度和框架特性。Vue 严格来说是 MVVM 吗?不完全。尤雨溪明确表示 Vue 受 MVVM 启发但不是严格的 MVVM 实现,原因如下:Model 不是独立层:Vue 的 Model 是普通 JS 对象(data),而非独立的领域模型抽象ViewModel 不是独立可测试层:Vue 实例同时包含响应式数据、生命周期钩子、方法等,与组件耦合提供了 $refs:$refs 允许直接操作 DOM 子组件,打破了 ViewModel 与 View 的隔离原则组件化优先于架构模式:Vue 的核心思想是组件化组合而非严格的分层架构所以 Vue 更准确的定位是响应式组件化框架,MVVM 只是它的灵感来源和教学标签。为什么现在新框架很少强调 MVVM 了?因为框架的竞争维度变了:2012-2016 年:框架竞争点是架构模式——AngularJS 推 MVC,Knockout 推 MVVM,Backbone 推 MVP,开发者需要被教怎么组织代码2016 年至今:架构模式已成共识,竞争点转向组件化、响应式粒度、编译优化、开发体验。React Hooks 让函数式组件替代 Class,Vue 3 Composition API 让逻辑复用更灵活,Svelte 用编译时抹掉运行时MVVM 作为概念依然存在(数据绑定仍是所有现代框架的基石),但不再是框架的卖点,而成了底层实现细节。面试中被问到时,重点是理解其解决耦合问题的思路,而非背诵定义。追问MVVM 的缺点是什么?调试困难:数据绑定是隐式的,数据流向不如命令式代码直观,Bug 定位需要追踪绑定链路内存开销:每个绑定都创建 Watcher,大量绑定会占用更多内存过度绑定风险:双向绑定容易导致数据流混乱,特别是跨组件通信时状态变更难以追溯不适合简单 UI:对于静态展示页面,引入 MVVM 框架是过度设计Vue 3 为什么从 Object.defineProperty 改用 Proxy?Object.defineProperty 有三个缺陷:无法监听属性的新增和删除(所以 Vue 2 需要 Vue.set / Vue.delete)无法监听数组索引和 length 变化(Vue 2 通过重写数组方法修补)需要递归遍历对象的每个属性,初始化性能差Proxy 可以代理整个对象,一次性解决以上所有问题,且是惰性响应式(只在访问时才递归代理子对象),初始化性能更好。这也是 Vue 3 大型项目性能提升的关键因素之一。面试中如何简洁地回答什么是 MVVM?三句话结构:MVVM 是 Model-View-ViewModel 架构模式,通过 ViewModel 的数据绑定让 View 和 Model 自动同步它解决了传统 MVC 中 View 和 Model 直接耦合的问题,用 ViewModel 把两者彻底解耦Vue 是典型实践,通过响应式系统拦截数据变化配合模板编译实现双向绑定,但 Vue 并非严格 MVVM(有 $refs 等例外)
前端阅读 715月28日 03:15

如何在 Vue 中实现事件总线 EventBus?

Vue 2 里用一个空 Vue 实例做中央事件通道,组件通过它发事件和听事件:// event-bus.jsimport Vue from 'vue';export const EventBus = new Vue();发送端 EventBus.$emit,接收端 EventBus.$on,销毁前必须 EventBus.$off 解绑——忘了这一步就是内存泄漏。典型场景:登录弹窗成功后通知导航栏更新头像,两个组件隔了好几层,props 传递不现实,EventBus 几行代码搞定。Vue 3 移除了 $on/$off,官方推荐用 mitt 替代,写法几乎一样:import mitt from 'mitt';export const bus = mitt();// 发送bus.emit('login-success', { user: 'xxx' });// 监听bus.on('login-success', handler);// 移除bus.off('login-success', handler);追问Vue 3 为什么移除 $on/$off?Vue 3 的响应式体系更推崇单向数据流和显式状态管理。EventBus 是隐式依赖——组件 A emit 一个事件,组件 B 在哪监听的完全不知道,出了 bug 追踪困难。Vue 团队认为这种模式弊大于利,干脆砍掉。如果确实需要事件模式,mitt 只有 200 字节,功能够用。EventBus 和 Pinia 怎么选?EventBus 适合"通知型"通信——A 告诉 B 发生了什么事,不需要持久化数据。Pinia 适合"状态型"通信——多个组件共享同一份数据,需要 devtools 调试和变更追溯。事件一多就别用 EventBus,散落在各组件里的 on/off 根本理不清,直接上 Pinia。为什么必须 $off?忘了会怎样?组件销毁了但 EventBus 实例还活着,回调引用还在,下次 emit 还会执行——操作一个已销毁组件的 this,轻则报错重则数据错乱。更严重的是 EventBus 持有回调闭包,闭包引用了组件实例,GC 无法回收,这就是内存泄漏。Vue 2 里如果用了箭头函数做 handler,$off 时传引用也解不掉,必须用命名函数。EventBus 事件名冲突怎么办?大型项目里十几个组件各自 emit/on,事件名撞了根本不会报错——只是 A 的数据莫名其妙到了 B。解法:统一在常量文件里定义事件名,禁止硬编码字符串。更好的做法是如果事件超过 5-6 个就别用 EventBus 了,换 Pinia。TypeScript 下 mitt 怎么做类型安全?mitt 支持泛型约束事件 map:type Events = { 'login-success': { user: string }; 'logout': undefined;};const bus = mitt<Events>();bus.emit('login-success', { user: 'xxx' }); // ✓bus.emit('wrong-name', {}); // ✗ 类型报错这样事件名和 payload 类型都有编译期检查,比 Vue 2 的 EventBus 安全得多。
前端阅读 1325月27日 01:06

Vue 3 如何在 setup 方法中获取组件实例?

Vue 3 Composition API 中,setup 里没有 this。要获取组件实例相关的能力,用对应的 API 而不是拿整个实例:获取 DOM 元素:const el = ref(null) + ref="el" 绑定,通过 el.value 访问获取路由实例:const router = useRouter(); const route = useRoute()获取 store:const store = useStore()(Pinia 或 Vuex 4)需要组件内部数据:直接在 setup 里定义 ref/reactive 并 return,模板可直接访问需要父组件数据:defineProps + defineEmits如果非拿实例不可(极少场景),Vue 3 提供了 getCurrentInstance():import { getCurrentInstance } from 'vue';const instance = getCurrentInstance();// instance.proxy 近似 Vue 2 的 this但官方不推荐依赖它,内部实现在版本间可能变化。追问Vue 3 为什么不给 this?Composition API 设计目标是函数式、可复用、类型推导友好。this 会绑定上下文,函数提取出去 this 就丢了。而 ref/reactive 是普通的 JS 值/对象,可以自由传递不丢失响应性。template ref 和 document.getElementById 怎么选?template ref 是 Vue 的方式——能保证在组件挂载后拿到元素,且不同组件实例互不干扰。document.getElementById 能拿到但可能拿到错误组件的元素(同页面多实例时),也不符合 Vue 的数据驱动理念。getCurrentInstance 在 setup 外能用吗?不能在 setup 外调用(如生命周期钩子的回调外)。它只在 setup 同步执行期间有效,异步回调里拿到的可能是 null。
前端阅读 725月27日 01:06

什么是双向绑定?Vue 是如何实现双向绑定功能的?

双向绑定 = 数据变化自动更新视图 + 用户输入自动更新数据。最典型的场景就是 <input v-model="msg">——你修改 input 的值,msg 跟着变;JS 里改 msg,input 显示跟着变。Vue 2 用 Object.defineProperty 劫持对象属性的 getter/setter 实现数据 → 视图的响应式,用 v-model 监听 input 事件实现视图 → 数据的反向同步。Vue 3 换成了 Proxy,能拦截更多操作(属性增删、数组索引),不需要像 Vue 2 那样用 $set 手动处理新增属性。追问v-model 的本质是什么?v-model="msg" 是语法糖,等价于 :value="msg" + @input="msg = $event.target.value"。对于 checkbox 是 :checked + @change,对于 select 是 :value + @change。双向绑定和单向数据流矛盾吗?不矛盾。Vue 仍然是单向数据流(父 → 子通过 props,子 → 父通过 emit)。v-model 只是父子通信的一种语法糖,本质上还是 prop + event 的模式。什么场景不适合用双向绑定?需要在赋值前做复杂转换的(用 computed + emit 更清晰)多个输入源同时修改一个数据的(容易互相覆盖)需要中间确认操作的(如提交时才正式更新)
前端阅读 12月7日 12:40

Vue.js中watchEffect函数的用途是什么?

watchEffect 函数在 Vue.js 中主要用于自动跟踪响应式状态的变化,并在每次状态更新时自动执行一段副作用代码。当任何在watchEffect中引用的响应式状态发生变化时,Vue 会自动重新执行此函数中的代码。这使得开发者能够在不需要显式声明依赖的情况下,轻松地响应状态变化。例如,可以使用watchEffect来执行与数据变化同步的操作,如数据验证、网络请求或是复杂的逻辑运算。这个函数的主要优点是简化了代码管理,不需要手动管理依赖项,Vue 会自动处理相关的依赖追踪和更新调度。
前端阅读 12月7日 11:57

Vue中渲染函数比模板有什么好处?

Vue中渲染函数相比于模板的好处包括:灵活性和动态性:渲染函数提供更高的灵活性和动态性,使开发者能够编写更动态的组件逻辑。在复杂的场景下,使用渲染函数可以根据不同的状态和条件动态生成不同的元素和结构。更细粒度的控制:通过渲染函数,开发者可以精确地控制每一个DOM节点的创建方式,属性和行为。这在处理复杂的用户交互和动态内容时尤其有用。JavaScript的完整能力:使用渲染函数意味着你可以使用JavaScript的全部特性来构建视图层,这包括高阶函数、条件语句、循环等,从而可以编写更加优雅和强大的代码。集成第三方库的便利:当需要在Vue组件中整合非Vue库或UI框架时,渲染函数可以更灵活地操作DOM,帮助更好地集成这些库。避免重复的模板结构:在需要根据不同情况重用相似但略有不同的DOM结构时,渲染函数可以通过编程的方式生成这些结构,避免了模板中的重复代码。优化性能:对于非常大或复杂的模板,使用渲染函数可以手动优化渲染过程,有时能够比自动生成的模板渲染代码更有效率。总的来说,渲染函数主要在需要高度灵活和动态的场景中显示出其优势,尤其适合高级开发者和复杂应用的开发。
前端阅读 12月6日 23:57

Vue中如何重用具有key属性的元素?

在Vue中,key属性主要用于Vue的虚拟DOM算法,以便跟踪可复用的元素。当你有一组元素,并且可能会动态地改变顺序或更新列表中的元素时,使用key可以帮助Vue准确地识别哪些元素是新的,哪些被重用了。这可以提高渲染性能并减少潜在的渲染错误。例如,如果你有一个列表,每个列表项都可能被更新或重新排序,你可以这样使用key:<div id="app"> <ul> <li v-for="item in items" :key="item.id"> {{ item.text }} </li> </ul></div>在这个例子中,每个li元素都被赋予了一个基于item.id的唯一key。当items数组发生变化时,Vue会使用key来确定哪些元素可以被保留,哪些需要重新创建。这种方法特别有用,比如在进行列表排序或替换列表中的项目时,因为Vue可以就地复用DOM结构和组件实例,避免不必要的元素重建和组件状态丢失。
前端阅读 12月6日 23:56

vue中如何在本地注册指令?

在Vue中,可以通过两种方式在组件中本地注册指令:全局注册和局部注册。局部注册指令要在单个组件中局部注册指令,你可以在组件的选项对象中使用 directives 属性。这里是一个基本的例子:<template> <div v-my-directive>我是一个有指令的元素</div></template><script>export default { directives: { 'my-directive': { bind(el, binding, vnode) { // 当指令第一次绑定到元素时调用 el.style.backgroundColor = 'yellow'; }, update(el, binding, vnode, oldVnode) { // 被绑定元素所在的模板更新时调用 if (binding.value !== binding.oldValue) { el.style.backgroundColor = binding.value; } } } }}</script>在上面的例子中:directives 对象中定义了一个名为 my-directive 的指令。bind 钩子在指令第一次绑定到元素时调用,这里用来设置元素的初始样式。update 钩子在包含该指令的组件的 VNode 更新时被调用,可以通过比较 binding.value 和 binding.oldValue 来决定是否需要更新元素的样式。这种方式的指令只能在声明它的那个组件内使用。
前端阅读 12月6日 23:56

vue中分组routes 到chunks中有什么优势?

在Vue中,将routes分组到chunks中主要有以下几个优势:性能优化:通过代码分割,只有在用户实际访问对应路由时,才会加载相应的chunk。这样可以减少应用初始加载时的文件大小,使得应用启动更快。按需加载:分组后的chunks可以实现按需加载,即按页面或功能模块加载资源,从而避免加载整个应用的所有脚本和资源,提升页面的响应速度和用户体验。缓存利用:将公共依赖和库分离到单独的chunks中,这些chunks可以被多个路由共享,并且可以被浏览器缓存起来。当用户访问其他页面时,若已缓存这些公共chunks,将不需要重新加载,这样可以减少网络请求,加快加载速度。简化调试:在开发过程中,如果每个路由的依赖都在独立的chunk中,当某个页面出现问题时,可以快速定位到对应的chunk,这样可以更加快捷和准确地进行bug修复和性能优化。更好的适应性和可维护性:随着应用规模的扩大,全部加载可能导致网页变得非常庞大。通过分组到chunks,可以将功能模块化,使得项目结构更清晰,便于管理和维护。
前端阅读 1212024年8月5日 12:48

Composition API 如何实现逻辑复用

在Vue.js的Composition API中,逻辑复用是通过使用可组合函数(composables)来实现的。可组合函数是可以封装和重用Vue组件逻辑的函数。Composition API引入了一种新的组织和重用组件逻辑的方式,它提供了更灵活的代码组织结构,使得函数的复用变得更加简单和清晰。要实现逻辑复用,你可以按照以下步骤操作:创建可组合函数(composables):你可以创建一个独立的JavaScript函数,这个函数利用Composition API中的ref, reactive, computed, watch, watchEffect等响应性API来创建和管理状态或逻辑。在组件中使用可组合函数:在Vue组件的setup函数中,你可以引入和使用这些可组合函数。这样,你就可以在多个组件之间共享和重用相同的逻辑,而无需复制代码。传递参数和返回值:可组合函数可以接受参数并返回一些响应式引用、方法或其他值,这使得它们可以与组件进行交互并根据组件的需要进行调整。下面我将通过一个简单的例子来说明这一过程:假设我们有一个用于处理用户信息的逻辑,这部分逻辑需要在多个组件中复用。我们可以创建一个名为useUser的可组合函数来封装这部分逻辑。// useUser.jsimport { ref } from 'vue';export function useUser() { const user = ref(null); const isLoading = ref(false); async function loadUser(userId) { isLoading.value = true; try { const response = await fetch(`/api/users/${userId}`); user.value = await response.json(); } catch (error) { console.error('Failed to load user', error); } finally { isLoading.value = false; } } return { user, isLoading, loadUser };}在上面的例子中,useUser函数创建了一个用户信息的响应式引用user和一个加载状态的响应式引用isLoading。它还提供了一个异步函数loadUser来加载用户数据。现在,我们可以在组件中使用这个可组合函数了:// UserProfile.vue<template> <!-- 使用user和isLoading渲染UI --></template><script>import { onMounted } from 'vue';import { useUser } from './useUser';export default { setup() { const { user, isLoading, loadUser } = useUser(); onMounted(() => { loadUser('123'); // 假设'123'是用户ID }); return { user, isLoading }; }};</script>在UserProfile.vue组件的setup函数中,我们引入并调用useUser可组合函数,并在组件被挂载时调用loadUser函数来加载用户数据。这样,user和isLoading就可以在组件的模板中直接使用了。这种方法不仅使得代码更加清晰和易于维护,而且还提高了代码的复用性。通过这种方式,我们可以将逻辑抽离出来,并在多个组件之间共享。