Vue 大厂高频面试题
一、 基础概念与框架设计
1. 谈谈你对 MVVM 开发模式的理解
MVVM(Model - View - ViewModel)是一种软件架构设计模式,特别适用于构建现代的用户界面应用程序,在前端开发领域应用广泛。它起源于微软 WPF 和 Silverlight 技术栈。
- 组件与职责:
- Model(模型层):代表应用程序的数据和业务逻辑部分,通常包含数据结构、数据获取与存储逻辑。它可以是数据库实体、API 响应或任何可以被视图模型操作的数据源。
- View(视图层):是用户可见的界面部分,负责展示数据和接收用户交互。在 MVVM 中,视图不直接处理业务逻辑,而是通过数据绑定与 ViewModel 进行交互。
- ViewModel(视图模型层):作为 Model 与 View 之间的桥梁,封装了视图的状态 and 行为逻辑。它对 Model 的数据进行处理,提供给 View 需要展示的形式,并将从 View 接收到的用户输入转换成对 Model 的操作指令。ViewModel 具有双向数据绑定的能力,即当 ViewModel 中的数据发生变化时,自动反映到关联的 View 上;同时,当用户通过 View 改变数据时,变化也会同步回 ViewModel。
- 优点:
- 低耦合性:由于数据绑定机制,ViewModel 无需知道具体的 View 实现细节,而 View 也不关心数据如何处理,二者只需关注自己的职责范围,降低了模块间的耦合度。
- 可测试性:ViewModel 仅包含业务逻辑和数据转换,易于编写单元测试以确保功能正确性。
- 代码复用和组织性:ViewModel 可以独立于视图设计,因此可在多个视图之间共享相同的业务逻辑,提高代码复用率。
- 分离关注点:MVVM 模式使得 UI 开发者可以专注于界面布局与样式,后端开发者可以专注于业务逻辑,两者协同工作更加高效。
- 动态更新:双向数据绑定使得 UI 能够实时响应数据变化,提供了更好的用户体验。
2. v-if 和 v-show 有什么区别
Vue.js 中的 v-if 和 v-show 都是用来控制元素是否渲染和显示在 DOM 中的指令,但它们的工作机制和使用场景有所不同:
- v-if:
- 功能:条件渲染。当表达式的值为 true 时,该元素及其包含的所有子元素会被渲染到 DOM 中;当表达式的值为 false 时,该元素将不会被渲染到 DOM 中。
- 性能:具有惰性渲染的特性,在初始渲染时如果条件为假,则不会执行任何渲染操作,直到条件变为真才会开始渲染。切换时涉及创建或销毁 DOM 元素,因此有较高的切换开销,但对初次渲染有优化效果。
- 使用场景:适合那些需要根据条件决定是否生成 DOM 的情况,或者在不需要渲染大量未使用的 DOM 时。
- v-show:
- 功能:条件显示。不论条件真假,元素总是会被渲染到 DOM 中,只是通过 CSS 的 display 属性来控制元素的可见性(display: none/block/flex/gird 等)。
- 性能:在初始渲染时会一次性编译并添加所有元素到 DOM 树中,之后只通过修改样式来切换元素的显示状态,所以其初始渲染开销可能较大,但在切换显示隐藏状态时,由于仅涉及 CSS 属性的改变,性能消耗相对较小。
- 使用场景:适用于频繁切换显示/隐藏状态,且 DOM 结构较为复杂的情况,因为避免了反复创建和销毁 DOM 元素带来的性能损耗。
3. 渐进式框架的理解
渐进式框架(Progressive Framework)的核心理念是允许开发者逐步增强或扩展应用程序的功能,而不是一次性提供一个全功能、一体化的解决方案。这种框架设计具有以下特点:
- 模块化:渐进式框架通常将功能划分为多个独立的模块或组件,每个模块可以单独引入和使用,这样开发者可以根据项目需求选择性地集成所需的部分。
- 低侵入性:它不会强制要求开发者遵循严格的规则集或重构整个应用才能使用框架,而是尽可能地与现有代码库和第三方库无缝融合。
- 灵活性:开发者能够轻松地从小规模开始,仅使用框架的基本功能,并随着项目的复杂度增加逐步添加更高级的功能。
- 可扩展性:框架的设计支持自定义扩展,这意味着开发者可以在需要时构建自己的插件或模块来满足特定业务需求。
- 易于上手:渐进式框架往往有一个较小的学习曲线,因为开发者可以从简单用例入手,随着对框架理解加深再逐渐深入到更复杂的特性和最佳实践。
以 Vue.js 为例,作为渐进式 JavaScript 框架,它可以灵活地应用于各种场景,从简单的页面交互到大型单页应用。开发者可以只使用其模板引擎和数据绑定特性进行开发,或者进一步利用 Vue 生态中的路由管理、状态管理等工具来构建更为复杂的系统。Vue 的设计使得开发者能够在不违背既有代码结构的前提下,逐步将 Vue 的功能融入到项目中去。
4. 常用的 vue 指令都有哪些
Vue.js 中常用的指令主要包括:
- v-if 和 v-else-if / v-else:
v-if:根据表达式的值决定是否渲染元素。v-else-if:在 v-if 之后使用,提供额外的条件判断。v-else:与 v-if 或者 v-else-if 配合使用,当前面的条件不满足时渲染内容。
- v-show:类似于 v-if,但不同之处在于它通过 CSS 的 display 属性控制元素显示/隐藏,DOM 元素始终会被渲染,只是切换可见性。
- v-for:用于循环渲染列表或数组数据,可以遍历数组、对象或生成迭代器的任何可迭代对象。
- v-bind(简写 :):绑定元素属性到实例数据,例如
v-bind:href="url"可以动态绑定链接地址,简写为:href="url"。 - v-on(简写 @):绑定事件,例如
v-on:click="method"可以绑定点击事件,简写为@click="method"。 - v-model:用于表单元素的双向数据绑定。
5. 说一下 Vue 的生命周期
- beforeCreate:是
new Vue()之后触发的第一个钩子,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。 - created:在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 Dom 进行交互,如果非要想,可以通过
vm.$nextTick来访问 Dom。 - beforeMount:发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 Dom 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated。
- mounted:在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点,使用
$refs属性对 Dom 进行操作。 - beforeUpdate:发生在更新之前,也就是响应式数据发生更新,虚拟 dom 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
- updated:发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
- beforeDestroy(Vue2)/ beforeUnmount(Vue3):发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
- destroyed(Vue2)/ unmounted(Vue3):发生在实例销毁之后,这个时候只剩下了 dom 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
- activated:keep-alive 专属,组件被激活时调用。
- deactivated:keep-alive 专属,组件被移除时调用。
- errorCaptured:错误被捕获时调用。
二、 高频考察原理
1. Vue 双向绑定原理
- Vue2:当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用
Object.defineProperty为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。一句话总结:vue.js 采用数据劫持结合发布-订阅模式,通过Object.defineproperty来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调。 - Vue3:使用了代理(Proxy)来替代 Vue2 中的
Object.defineProperty实现数据的响应式。具体来说,当数据被初始化时,Vue3 会利用 ES6 的 Proxy 对象来代理数据对象的所有操作。通过 Proxy 可以拦截数据的读取 and 修改操作,并且自动追踪依赖 and 触发更新。Vue3 引入了reactiveandrefAPI 来创建响应式对象 and 响应式引用。使用effect函数来追踪副作用(例如视图的更新),当依赖的数据变化时,effect会自动重新执行。
2. 虚拟 DOM 实现原理
虚拟 DOM 是用 JavaScript 对象来模拟真实 DOM 树的一种轻量级 JavaScript 对象。它是实现 vue 和 React 的重要基石。
- 原理:用 js 模拟 DOM 结构,计算出最小的变更,操作 DOM。通过 snabbdom 学习 vdom,Vue 参考它实现的 vdom 和 diff。
- diff 算法:是 vdom 中最核心、最关键的部分。树 diff 的时间复杂度原本为 ,通过优化,只比较同一层级,不跨级比较;如果节点不同,则直接删掉重建,不再深度比较;tag 和 key 两者都相同,则认为是相同节点,不再深度比较。
- key 的作用:key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速(对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位)。
3. computed 的实现原理
computed 本质是一个惰性求值的观察者。computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。其内部通过 this.dirty 属性标记计算属性是否需要重新求值。当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,computed watcher 通过 this.dep.subs.length 判断有没有订阅者,有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染(Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化);没有的话,仅仅把 this.dirty = true(当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性)。
4. computed 和 watch 有什么区别及运用场景
- 区别:
- computed 计算属性:依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
- watch 侦听器:更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
- 运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。
- 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作(访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
5. 为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty
- Object.defineProperty 的局限性:
- 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动)。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组:
push()、pop()、shift()、unshift()、splice()、sort()、reverse()。由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。 - 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue2.x 里,是通过递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是更好的选择。
- 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动)。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组:
- Proxy 的优势:可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
三、 实践与工程化相关
1. Vue 组件间通信有哪些方式
- 父子组件通信:
props:父组件向子组件传递数据。$emit:子组件向父组件传递数据,通过触发自定义事件。ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。$parent / $children:访问父 / 子实例。
- 隔代组件通信:
$attrs / $listeners:$attrs包含了父作用域中不被 prop 所识别(且获取)的特性绑定。$listeners包含了父作用域中的 v-on 事件监听器(不含 .native 修饰器)。可以通过v-bind="$attrs"和v-on="$listeners"传入内部组件。provide / inject:祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。主要解决了跨级组件间的通信问题。
- 兄弟组件通信:
- EventBus ($emit / $on):通过一个空的 Vue 实例作为中央事件总线(事件中心)。
- Vuex / Pinia:用于全局状态管理,适用于复杂的应用场景。
2. vue-router 有哪些钩子函数
- 全局导航钩子:
router.beforeEach(to, from, next):全局前置守卫。router.beforeResolve(to, from, next):全局解析守卫。router.afterEach(to, from, next):全局后置守卫。
- 路由独享及组件内钩子:
beforeEnter:路由配置内的守卫。beforeRouteEnter:组件内进入路由。beforeRouteUpdate:组件内路由更新。beforeRouteLeave:组件内离开路由。
3. Vuex 有哪几种属性
- state:用来存放共享变量的地方。
- getter:派生状态(相当于 store 中的计算属性),用来获得共享变量的值。
- mutations:用来存放修改 state 的方法,必须是同步操作。
- actions:常用来做一些异步操作,在 mutations 的基础上执行。
- modules:允许我们将 store 分割成模块(module),每个模块拥有自己的 state、mutations 等。
4. Vue 首屏加载优化
- 代码处理:
- 压缩代码:使用 UglifyJsPlugin 等工具缩减体积。
- 路由懒加载:分割代码块,按需加载。
- 按需加载组件:减少初始资源加载。
- 资源与渲染:
- CDN 加速:托管静态资源到 CDN 节点。
- 使用
<KeepAlive>:缓存不经常切换的组件。 - 使用
<Suspense>:处理异步组件加载状态。 - 优化资源加载:如图片懒加载。
- 性能指令:
- 使用
v-once或v-if:避免不必要的渲染。
- 使用
5. 如何在 Vue 应用中进行表单处理和验证
- 表单处理:使用
v-model进行双向绑定。 - 表单验证:
- 使用第三方库:如
VeeValidate或Vue-Form-Validation。 - 手动编写验证逻辑:在提交前检查表单状态。
- 利用计算属性和指令:结合响应式数据和自定义逻辑实现验证。
- 使用第三方库:如