哈喽,大家好,我是一个学前端的前端,来来来,小板凳准备好,划重点~,今天来聊聊关于我的面试总结,希望对你有帮助
我:你好,面试官,我是......
面试官:你来说说......
最后:没有最后,啊哈哈哈哈哈
1. 面试官:谈一下你对Vue响应式原理的理解?
这是Vue最核心的特性。Vue 2主要通过Object.defineProperty实现。
-
核心过程:
- 数据劫持:初始化时,Vue会遍历data中的属性,用Object.defineProperty将它们转为getter/setter,在属性被访问和修改时拦截。
- 依赖收集:在getter中,会将当前正在计算的Watcher(如组件的render函数)添加为该属性的订阅者。
- 派发更新:在setter被触发时,会通知所有相关的Watcher,Watcher再触发组件的重新渲染。
-
注意事项:Vue 2无法检测到对象属性的直接添加或删除(需用Vue.set/Vue.delete),也无法监听数组索引和长度变化(需使用数组变异方法如push)。
2. 面试官:Vue 2中如何进行数组的响应式更新?
Vue 2对数组做了特殊处理。
- 原理:它重写了数组的7个变更方法(push, pop, shift, unshift, splice, sort, reverse)。当你调用这些方法时,Vue除了执行原始操作,还会触发视图更新。
- 注意事项:直接通过索引修改数组元素(arr[index] = newValue)或直接修改数组长度(arr.length = newLength)不会触发更新。解决方案是使用Vue.set(arr, index, newValue)或splice方法。
3.面试官: Vue的虚拟DOM是什么?请描述Diff算法过程。
虚拟DOM是一个用JS对象(VNode)描述的DOM树副本。
-
作用:通过新旧VNode比较(Diff),计算出最小的DOM操作,提升性能。
-
Diff过程(同层级比较):
-
节点比较:新旧节点不同(tag、key等),直接替换。
-
相同节点比较:
- 更新属性:对比并更新节点的class、style等属性。
-
比较子节点(核心):
- Vue使用双端比较算法,通过新旧子节点的首尾指针进行4次快速比对(新前-旧前,新后-旧后,新后-旧前,新前-旧后)。
- 若都未命中,则通过key建立旧子节点的索引图,用新节点的key去查找可复用的旧节点,然后进行移动或创建。
-
4. 面试官:Vue的生命周期有哪些?created和mounted的区别?
Vue实例从创建到销毁的过程。
-
主要阶段:
- 创建阶段:beforeCreate(实例初始化前,无法访问data/methods) -> created(实例创建完成,可访问data/methods,常用于异步请求) -> beforeMount(挂载前) -> mounted(DOM挂载完成,可操作DOM)。
- 更新阶段:beforeUpdate(数据更新,DOM未更新) -> updated(DOM已更新)。
- 销毁阶段:beforeDestroy(实例销毁前,常用于清理定时器、解绑事件) -> destroyed(实例销毁后)。
-
核心区别:
- created:数据观测已完成,DOM未生成,不能操作DOM。
- mounted:DOM已挂载,可以操作DOM。
5.面试官:为什么v-for要绑定key?为什么不能用index作为key?
key的作用:帮助Vue在虚拟DOM Diff过程中高效地识别节点的身份和复用关系。没有key时,Vue会采用“就地更新”策略,可能导致错误的节点被复用,带来状态更新问题。- 为何避免用
index:因为index不具有稳定性和唯一性。当列表顺序发生变化(如增、删、排序)时,同一个index指向的数据项已经改变。Vue会误以为可以复用旧的DOM节点,导致更新错误(如本该更新的节点没有更新,或节点状态错乱)。
6.面试官:computed和watch的区别与使用场景?
-
computed(计算属性):特点:是基于响应式依赖进行缓存的计算值。只有依赖发生变化,才会重新计算。不支持异步操作。
场景:适合进行同步计算,并将结果作为属性使用,如:fullName = firstName + lastName。
-
watch(侦听器):特点:监听一个特定响应式数据的变化,并在变化时执行异步或开销较大的操作。支持配置immediate(立即执行)和deep(深度监听)。
场景:适合在数据变化时执行异步请求、复杂逻辑或副作用操作,如:搜索建议、表单验证。
7.面试官:简述Vue的组件通信方式。
常用方式如下,适用于不同场景。
| 方式 | 通信方向 | 说明 | 使用场景 |
|---|---|---|---|
Props / $emit | 父 <-> 子 | 父通过props传数据给子,子通过$emit触发事件给父 | 最常用、最基础的父子通信 |
$refs / $parent / $children | 父 <-> 子 | 直接访问组件实例 | 操作子组件方法/数据,但耦合度高 |
$attrs / $listeners | 父 <-> 子 (跨级) | listeners:接收事件 | 创建高级组件(如二次封装UI组件) |
| Event Bus | 任意组件 | 创建一个空的Vue实例作为中央事件总线 | 小型项目、简单场景的跨级/兄弟通信 |
provide / inject | 祖先 -> 后代 | 祖先组件provide提供数据,后代组件inject注入 | 库/高阶组件开发,有明确层级关系 |
| Vuex | 任意组件 | 全局状态管理库,集中式存储管理 | 中大型项目,复杂数据流管理 |
8.面试官:$nextTick的原理和作用是什么?
-
作用:将回调函数延迟到下次DOM更新循环之后执行。用于确保能获取到数据更新后的最新DOM。
-
原理:
- Vue的DOM更新是异步队列。当你修改数据后,视图不会立即更新,而是将更新作为一个微任务(在Vue 2中优先使用Promise.then,降级到setImmediate或setTimeout)推入队列。
$nextTick接受一个回调函数,也会被推入同一个队列。当同步代码执行完毕,会依次清空这个微任务队列,先执行DOM更新任务,再执行$nextTick的回调,从而拿到更新后的DOM。
9.面试官:说一下Vue 2和Vue 3在响应式实现上的主要区别?
| 特征 | vue2 | vue3 |
|---|---|---|
| 实现方式 | Object.defineProperty (ES5) | Proxy (ES6) |
| 监听范围 | 对对象的每个属性进行劫持 | 直接代理整个对象 |
| 数组监听 | 需要重写数组方法,无法直接监听索引变化 | 可直接监听数组索引和长度变化 |
| 动态新增属性 | 默认无法监听,需用Vue.set | 可以直接监听 |
| 性能与初始化 | 需要递归遍历初始数据,性能开销大 | 惰性代理,访问时才进行响应式转换,性能更优 |
10.面试官:Vue中如何实现一个自定义指令?
自定义指令的核心是定义一组生命周期钩子函数。
-
注册方式:
- 全局注册:
Vue.directive('focus', { ... })
- 局部注册:在组件选项中定义
directives: { focus: { ... } }
- 全局注册:
-
常用钩子函数:
- bind:指令第一次绑定到元素时调用(只调用一次)。
- inserted:被绑定元素插入父节点时调用(不一定已插入文档流)。
- update:所在组件的VNode更新时调用,可能发生在子VNode更新之前。
- componentUpdated:所在组件的VNode及其子VNode全部更新后调用。
- unbind:指令与元素解绑时调用(只调用一次)。
- 示例(实现一个输入框自动聚焦指令):
Vue.directive('focus', {
inserted: function (el) {
el.focus()
}
})
<input v-focus>
11.面试官:Vue.mixin 的使用场景、实现原理与潜在问题
Vue.mixin 是一种分发组件可复用功能的灵活方式,用于全局混入代码逻辑。
-
使用场景:适用于需要给大量组件注入相同逻辑的情况,例如统一的埋点方法、公共的工具函数或特定的生命周期处理。
-
实现原理:
- 在组件初始化时,Vue会将mixin对象与组件自身的选项进行合并。
- 对于数据对象(如data),会进行递归合并,以组件数据优先。
- 对于同名钩子函数(如created),会被合并成一个数组,混入的钩子先执行。
- 值为对象的选项(如methods, components)会被合并为同一个对象,键名冲突时以组件优先。
-
潜在问题:
- 来源不明确:混入的属性/方法在多个组件中使用时,难以快速定位其定义位置,增加维护和调试成本。
- 命名冲突:多个混入或与组件自身选项容易产生命名冲突,可能导致意外覆盖。
- 滥用导致关系复杂:过度使用会使组件逻辑变得不透明,数据流难以追踪。
-
替代方案:对于可复用的业务逻辑,更推荐使用组合式函数(在Vue 2中可通过独立模块模拟)或基于Vue.extend的高阶组件。
12.面试官:Vue 组件中 data 为什么必须是函数?
这是为了确保每个组件实例拥有独立的数据副本,防止数据污染。
- 根本原因:组件是可复用的。如果data直接是一个对象,那么所有使用这个组件的实例将共享引用同一个数据对象。修改其中一个实例的数据,会直接影响所有其他实例。
- 函数的作用:当data是一个函数时,Vue在创建每个组件实例时都会调用这个函数,返回一个全新的数据对象,从而实现了实例间数据的隔离。
- 源码层面的简化解释:在组件初始化过程中,Vue会执行mergeOptions策略。对于data选项,会判断其是否为函数,如果是,则通过调用getData来获取独立的数据对象。
13.面试官:Vue.extend 与 Vue.component 的区别及用法
两者都用于组件注册,但层级和用途不同。
Vue.extend(options):
- 作用:创建一个Vue组件的“子类”或“构造器”。它接收一个组件选项对象,返回一个可复用的组件构造函数(VueComponent)。
- 用法:常用于编程式地创建组件实例。例如,在需要动态挂载一个弹窗组件到body时:
const MessageConstructor = Vue.extend(MessageComponent)
const instance = new MessageConstructor({ propsData: { text: 'Hello' } })
instance.$mount() // 可以挂载到指定DOM元素
document.body.appendChild(instance.$el)
Vue.component(id, [definition]):
- 作用:注册或获取一个全局组件。它让组件可以在任何Vue实例的模板中通过标签名使用。
- 用法:definition可以直接是一个选项对象(Vue会在内部自动调用Vue.extend),也可以是一个已经由Vue.extend创建好的构造函数。
// 方式一:传入选项对象
Vue.component('my-button', { template: '<button>Click</button>' })
// 方式二:传入构造器
const MyButton = Vue.extend({ ... })
Vue.component('my-button', MyButton)
14.面试官:如何在Vue 2项目中实现性能优化?
Vue项目性能优化需多维度考虑。
| 优化方 向 | 具体措施 | 原理与效果 |
|---|---|---|
| 代码层面 | 1. 路由懒加载:使用import()动态导入组件。 2. 第三方库按需引入:如lodash的lodash-es。 3. 合理使用v-if和v-show。 | 减少初始包体积,提升首屏加载速度。 |
| 运行时层面 | 1. 避免v-for和v-if联用。 2. 精细化v-for的key,避免用index。 3. 使用Object.freeze冻结大数据。 | 减少不必要的计算和DOM操作,提升渲染效率。 |
| 构建层面 | 1. 利用Webpack的SplitChunks拆包。 2. 使用uglifyjs-webpack-plugin或terser-webpack-plugin压缩混淆。 3. 配置Gzip/Brotli压缩。 | 优化资源加载和传输效率。 |
| 高级特性 | 1. 使用keep-alive缓存不活跃组件。 2. 对于纯展示组件,使用functional函数式组件。 | 减少组件创建和销毁开销,提升复用。 |
15.面试官:解释一下 Vue 的模板编译过程
Vue的模板(.vue文件中的<template>或template选项)会经历一个从字符串到渲染函数的编译过程,主要分为三步:
- 解析(Parse):使用正则和词法分析,将模板字符串解析成一个抽象语法树(AST)。AST是一个用JavaScript对象描述的节点树,记录了标签名、属性、父子关系等所有模板信息。
- 优化(Optimize):静态标记。遍历AST,找出所有静态节点(即不依赖于响应式数据的节点,如
<div>静态文本</div>)和静态根节点,并为它们打上标记。在后续的patch(虚拟DOM对比)过程中,Vue会跳过这些静态节点,从而提升性能。 - 代码生成(Generate):将优化后的AST转换(编译)成可执行的渲染函数(
renderfunction)的代码字符串。这个渲染函数在被调用时会返回虚拟DOM(VNode)。
最终产物:一个render函数,形式大致如下:
with(this) {
return _c('div', { attrs: { id: 'app' } }, [
_c('p', [_v('Hello ' + _s(name))]) // _c创建元素, _v创建文本, _s转字符串
])
}